From effb405cae88474c27f5c8322a2627019af1cf64 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 14 May 2024 22:21:38 +0800
Subject: [PATCH 001/131] Always load or generate oauth2 jwt secret (#30942)

Fix #30923
---
 modules/setting/oauth2.go      | 17 ++++++-----------
 modules/setting/oauth2_test.go | 28 +++++++++++++++++++++++++++-
 routers/install/install.go     | 11 +++++++++++
 3 files changed, 44 insertions(+), 12 deletions(-)

diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index e59f54420b..0d3e63e0b4 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -126,16 +126,15 @@ func loadOAuth2From(rootCfg ConfigProvider) {
 		OAuth2.Enabled = sec.Key("ENABLE").MustBool(OAuth2.Enabled)
 	}
 
-	if !OAuth2.Enabled {
-		return
-	}
-
-	jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
-
 	if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
 		OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
 	}
 
+	// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
+	// Because this secret is also used as GeneralTokenSigningSecret (as a quick not-that-breaking fix for some legacy problems).
+	// Including: CSRF token, account validation token, etc ...
+	// In main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
+	jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
 	if InstallLock {
 		jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
 		if err != nil {
@@ -157,8 +156,6 @@ func loadOAuth2From(rootCfg ConfigProvider) {
 	}
 }
 
-// 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 {
@@ -166,11 +163,9 @@ func GetGeneralTokenSigningSecret() []byte {
 	if old == nil || len(*old) == 0 {
 		jwtSecret, _, err := generate.NewJwtSecretWithBase64()
 		if err != nil {
-			log.Fatal("Unable to generate general JWT secret: %s", err.Error())
+			log.Fatal("Unable to generate general JWT secret: %v", err)
 		}
 		if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
-			// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
-			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/oauth2_test.go b/modules/setting/oauth2_test.go
index 4403f35892..38ee4d248d 100644
--- a/modules/setting/oauth2_test.go
+++ b/modules/setting/oauth2_test.go
@@ -4,6 +4,7 @@
 package setting
 
 import (
+	"os"
 	"testing"
 
 	"code.gitea.io/gitea/modules/generate"
@@ -14,7 +15,7 @@ import (
 
 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())
+	generalSigningSecret.Store(nil)
 	s1 := GetGeneralTokenSigningSecret()
 	assert.NotNil(t, s1)
 	s2 := GetGeneralTokenSigningSecret()
@@ -33,6 +34,31 @@ JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
 	assert.EqualValues(t, expected, actual)
 }
 
+func TestGetGeneralSigningSecretSave(t *testing.T) {
+	defer test.MockVariableValue(&InstallLock, true)()
+
+	old := GetGeneralTokenSigningSecret()
+	assert.Len(t, old, 32)
+
+	tmpFile := t.TempDir() + "/app.ini"
+	_ = os.WriteFile(tmpFile, nil, 0o644)
+	cfg, _ := NewConfigProviderFromFile(tmpFile)
+	loadOAuth2From(cfg)
+	generated := GetGeneralTokenSigningSecret()
+	assert.Len(t, generated, 32)
+	assert.NotEqual(t, old, generated)
+
+	generalSigningSecret.Store(nil)
+	cfg, _ = NewConfigProviderFromFile(tmpFile)
+	loadOAuth2From(cfg)
+	again := GetGeneralTokenSigningSecret()
+	assert.Equal(t, generated, again)
+
+	iniContent, err := os.ReadFile(tmpFile)
+	assert.NoError(t, err)
+	assert.Contains(t, string(iniContent), "JWT_SECRET = ")
+}
+
 func TestOauth2DefaultApplications(t *testing.T) {
 	cfg, _ := NewConfigProviderFromData(``)
 	loadOAuth2From(cfg)
diff --git a/routers/install/install.go b/routers/install/install.go
index 9c6a8849b6..fde8b37ed5 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -481,6 +481,17 @@ func SubmitInstall(ctx *context.Context) {
 		cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
 	}
 
+	// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
+	// see the "loadOAuth2From" in "setting/oauth2.go"
+	if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") {
+		_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
+		if err != nil {
+			ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
+			return
+		}
+		cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
+	}
+
 	// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
 	if setting.SecretKey == "" {
 		var secretKey string

From 5b6f80989fbd0574ca188ab683389ff7659de30d Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 15 May 2024 07:06:12 +0800
Subject: [PATCH 002/131] Remove unnecessary double quotes on language file
 (#30977)

The double quotes and the prefix/suffix space are unnecessary.

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 options/locale/locale_en-US.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6a08041a7c..a85b107eee 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3348,7 +3348,7 @@ mirror_sync_create = synced new reference <a href="%[2]s">%[3]s</a> to <a href="
 mirror_sync_delete = synced and deleted reference <code>%[2]s</code> at <a href="%[1]s">%[3]s</a> from mirror
 approve_pull_request = `approved <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request = `suggested changes for <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release = `released <a href="%[2]s"> "%[4]s" </a> at <a href="%[1]s">%[3]s</a>`
+publish_release = `released <a href="%[2]s">%[4]s</a> at <a href="%[1]s">%[3]s</a>`
 review_dismissed = `dismissed review from <b>%[4]s</b> for <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason = Reason:
 create_branch = created branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>

From db578431ea5e8dc7347ba3dc10e82a01c5ba3ace Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Wed, 15 May 2024 00:25:44 +0000
Subject: [PATCH 003/131] [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_fr-FR.ini |   1 -
 options/locale/locale_it-IT.ini |   1 -
 options/locale/locale_ja-JP.ini |   1 -
 options/locale/locale_lv-LV.ini | 114 ++++++++++++++++----------------
 options/locale/locale_pt-BR.ini |   1 -
 options/locale/locale_pt-PT.ini |   1 -
 options/locale/locale_ru-RU.ini |   1 -
 options/locale/locale_si-LK.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 -
 17 files changed, 57 insertions(+), 73 deletions(-)

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 82d7867168..6314b62f66 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -3320,7 +3320,6 @@ mirror_sync_create=synchronizoval/a novou referenci <a href="%[2]s">%[3]s</a> do
 mirror_sync_delete=synchronizoval/a a smazal/a referenci <code>%[2]s</code> v <a href="%[1]s">%[3]s</a> ze zrcadla
 approve_pull_request=`schválil/a <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`navrhl/a změny pro <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`vydal/a <a href="%[2]s"> "%[4]s" </a> v <a href="%[1]s">%[3]s</a>`
 review_dismissed=`zamítl/a posouzení z <b>%[4]s</b> pro <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Důvod:
 create_branch=vytvořil/a větev <a href="%[2]s">%[3]s</a> v <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index dd2b34a6f4..5bca84ca08 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -3329,7 +3329,6 @@ mirror_sync_create=neue Referenz <a href="%[2]s">%[3]s</a> bei <a href="%[1]s">%
 mirror_sync_delete=hat die Referenz des Mirrors <code>%[2]s</code> in <a href="%[1]s">%[3]s</a> synchronisiert und gelöscht
 approve_pull_request=`hat <a href="%[1]s">%[3]s#%[2]s</a> approved`
 reject_pull_request=`schlug Änderungen für <a href="%[1]s">%[3]s#%[2]s</a> vor`
-publish_release=`veröffentlichte Release <a href="%[2]s"> "%[4]s" </a> in <a href="%[1]s">%[3]s</a>`
 review_dismissed=`verwarf das Review von <b>%[4]s</b> in <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Grund:
 create_branch=legte den Branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a> an
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 9553ba2f3a..834d1d7d70 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -3210,7 +3210,6 @@ mirror_sync_create=συγχρονίστηκε η νέα αναφορά <a href="
 mirror_sync_delete=συγχρόνισε και διάγραψε την αναφορά <code>%[2]s</code> σε <a href="%[1]s">%[3]s</a> από το είδωλο
 approve_pull_request=`ενέκρινε το <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`πρότεινε αλλαγές για το <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`έκδωσε τη <a href="%[2]s"> "%[4]s" </a> στο <a href="%[1]s">%[3]s</a>`
 review_dismissed=`ακύρωσε την εξέταση από <b>%[4]s</b> for <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Αιτία:
 create_branch=δημιούργησε το κλαδο <a href="%[2]s">%[3]s</a> στο <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index f3e2d93e80..3894e0e85b 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -3193,7 +3193,6 @@ mirror_sync_create=sincronizó la nueva referencia <a href="%[2]s">%[3]s</a> a <
 mirror_sync_delete=sincronizada y eliminada referencia <code>%[2]s</code> en <a href="%[1]s">%[3]s</a> desde réplica
 approve_pull_request=`aprobó <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`sugirió cambios para <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`se lanzó <a href="%[2]s"> "%[4]s" </a> en <a href="%[1]s">%[3]s</a>`
 review_dismissed=`descartó la revisión de <b>%[4]s</b> para <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Motivo:
 create_branch=creó rama <a href="%[2]s">%[3]s</a> en <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 25a3361b3f..d720ecf2f8 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -2508,7 +2508,6 @@ mirror_sync_create=مرجع جدید <a href="%[2]s">%[3]s</a> با <a href="%[1
 mirror_sync_delete=از مرجع <code>%[2]s</code> در<a href="%[1]s">%[3]s</a> حذف شده و از قرینه همگام شده
 approve_pull_request=`تأیید <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`تغییرات پیشنهادی برای <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`<a href="%[2]s"> "%[4]s" </a> در <a href="%[1]s">%[3]s</a> منتشر شد`
 review_dismissed=`بازبینی از <b>%[4]s</b> برای <a href="%[1]s">%[3]s#%[2]s</a> رد شد`
 review_dismissed_reason=دلیل:
 create_branch=شاخه <a href="%[2]s">%[3]s</a> در <a href="%[1]s">%[4]s</a> ایجاد کرد
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index b90039c003..556fab28e8 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -3249,7 +3249,6 @@ mirror_sync_create=a synchronisé la nouvelle référence <a href="%[2]s">%[3]s<
 mirror_sync_delete=a synchronisé puis supprimé la nouvelle référence <code>%[2]s</code> vers <a href="%[1]s">%[3]s</a> depuis le miroir
 approve_pull_request=`a approuvé <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`a suggérés des changements pour <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`a publié <a href="%[2]s"> "%[4]s" </a> dans <a href="%[1]s">%[3]s</a>`
 review_dismissed=`a révoqué l’évaluation de <b>%[4]s</b> dans <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Raison :
 create_branch=a créé la branche <a href="%[2]s">%[3]s</a> dans <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index eceda0faad..0cecc0b7f3 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -2707,7 +2707,6 @@ mirror_sync_create=ha sincronizzato un nuovo riferimento <a href="%[2]s">%[3]s</
 mirror_sync_delete=riferimento sincronizzato ed eliminato <code>%[2]s</code> a <a href="%[1]s">%[3]s</a> dal mirror
 approve_pull_request=`ha approvato <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`ha suggerito modifiche per <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`ha rilasciato <a href="%[2]s"> "%[4]s" </a> su <a href="%[1]s">%[3]s</a>`
 review_dismissed=`respinta la recensione da <b>%[4]s</b> per <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Motivo:
 create_branch=ha creato il ramo <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 03a06dab16..cf9d9bbc51 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -3344,7 +3344,6 @@ mirror_sync_create=が <a href="%[1]s">%[4]s</a> の新しい参照 <a href="%[2
 mirror_sync_delete=が <a href="%[1]s">%[3]s</a> の参照 <code>%[2]s</code> をミラーから反映し、削除しました
 approve_pull_request=`が <a href="%[1]s">%[3]s#%[2]s</a> を承認しました`
 reject_pull_request=`が <a href="%[1]s">%[3]s#%[2]s</a>について変更を提案しました`
-publish_release=`が <a href="%[1]s">%[3]s</a> の <a href="%[2]s"> "%[4]s" </a> をリリースしました`
 review_dismissed=`が <b>%[4]s</b> の <a href="%[1]s">%[3]s#%[2]s</a> へのレビューを棄却しました`
 review_dismissed_reason=理由:
 create_branch=がブランチ <a href="%[2]s">%[3]s</a> を <a href="%[1]s">%[4]s</a> に作成しました
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 3aed4bd6c5..bdfe3f8c9f 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -111,7 +111,7 @@ preview=Priekšskatītījums
 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.
+error404=Lapa, ko tiek mēģināts atvērt, vai nu <strong>nepastāv</strong> vai arī <strong>nav tiesību</strong> to aplūkot.
 go_back=Atgriezties
 
 never=Nekad
@@ -133,10 +133,10 @@ 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ā
+show_full_screen=Rādīt pilnekrānā
 download_logs=Lejupielādēt žurnālus
 
-confirm_delete_selected=Apstiprināt, lai izdzēstu visus atlasītos vienumus?
+confirm_delete_selected=Apstiprināt visu atlasīto vienumus dzēšanu?
 
 name=Nosaukums
 value=Vērtība
@@ -651,10 +651,11 @@ cancel=Atcelt
 language=Valoda
 ui=Motīvs
 hidden_comment_types=Attēlojot paslēpt šauds komentārus:
+hidden_comment_types_description=Komentāru veidi, kas atzīmēti, netiks rādīti problēmu lapā. Piemēram, atzīmējot "Iezīmes" netiks rādīti komentāri "{lietotājs} pievienoja/noņēma {iezīme} iezīmi".
 hidden_comment_types.ref_tooltip=Komentāri, kad problēmai tiek pievienota atsauce uz citu probēmu, komentāru, …
 hidden_comment_types.issue_ref_tooltip=Komentāri par lietotāja izmaiņām ar problēmas saistīto atzaru/tagu
 comment_type_group_reference=Atsauces
-comment_type_group_label=Etiķetes
+comment_type_group_label=Iezīmes
 comment_type_group_milestone=Atskaites punktus
 comment_type_group_assignee=Atbildīgos
 comment_type_group_title=Nosaukuma izmaiņas
@@ -956,8 +957,8 @@ repo_desc_helper=Ievadiet īsu aprakstu (neobligāts)
 repo_lang=Valoda
 repo_gitignore_helper=Izvēlieties .gitignore sagatavi.
 repo_gitignore_helper_desc=Izvēlieties kādi faili netiks glabāti repozitorijā no sagatavēm biežāk lietotājām valodām. Pēc noklusējuma .gitignore iekļauj valodu kompilācijas rīku artifaktus.
-issue_labels=Problēmu etiķetes
-issue_labels_helper=Izvēlieties problēmu etiķešu kopu.
+issue_labels=Problēmu iezīmes
+issue_labels_helper=Izvēlieties problēmu iezīmju kopu.
 license=Licence
 license_helper=Izvēlieties licences failu.
 license_helper_desc=Licence nosaka, ko citi var un ko nevar darīt ar šo kodu. Neesat pārliecintāts, kādu izvēlēties šim projektam? Aplūkojiet <a target="_blank" rel="noopener noreferrer" href="%s">licences izvēle</a>.
@@ -1030,15 +1031,15 @@ desc.internal=Iekšējs
 desc.archived=Arhivēts
 desc.sha256=SHA256
 
-template.items=Sagataves ieraksti
+template.items=Sagataves vienumi
 template.git_content=Git saturs (noklusētais atzars)
 template.git_hooks=Git āķi
 template.git_hooks_tooltip=Pēc repozitorija izveidošanas, Jums nav tiesību mainīt Git āķus. Atzīmējiet šo tikai, ja uzticaties sagataves repozitorija saturam.
 template.webhooks=Tīmekļa āķi
 template.topics=Tēmas
 template.avatar=Profila attēls
-template.issue_labels=Problēmu etiķetes
-template.one_item=Norādiet vismaz vienu sagataves vienību
+template.issue_labels=Problēmu iezīmes
+template.one_item=Norādiet vismaz vienu sagataves vienumu
 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.
@@ -1060,10 +1061,10 @@ 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=Vienumi, ko pārņemt
 migrate_items_wiki=Vikivietni
 migrate_items_milestones=Atskaites punktus
-migrate_items_labels=Etiķetes
+migrate_items_labels=Iezīmes
 migrate_items_issues=Problēmas
 migrate_items_pullrequests=Izmaiņu pieprasījumus
 migrate_items_merge_requests=Sapludināšanas pieprasījumi
@@ -1078,7 +1079,7 @@ migrate.permission_denied_blocked=Nav iespējams importēt no neatļautām adres
 migrate.invalid_local_path=Nederīgs lokālais ceļš. Tas neeksistē vai nav direktorija.
 migrate.invalid_lfs_endpoint=LFS galapunkts nav korekts.
 migrate.failed=Migrācija neizdevās: %v
-migrate.migrate_items_options=Piekļuves pilnvara ir nepieciešams, lai migrētu papildus datus
+migrate.migrate_items_options=Piekļuves pilnvara ir nepieciešama, lai pārņemtu papildus datus
 migrated_from=Migrēts no <a href="%[1]s">%[2]s</a>
 migrated_from_fake=Migrēts no %[1]s
 migrate.migrate=Migrēt no %s
@@ -1097,7 +1098,7 @@ migrate.gitbucket.description=Migrēt datus no GitBucket instancēm.
 migrate.migrating_git=Migrē git datus
 migrate.migrating_topics=Migrē tēmas
 migrate.migrating_milestones=Migrē atskaites punktus
-migrate.migrating_labels=Migrē etiķetes
+migrate.migrating_labels=Migrē iezīmes
 migrate.migrating_releases=Migrē laidienus
 migrate.migrating_issues=Migrācijas problēmas
 migrate.migrating_pulls=Migrē izmaiņu pieprasījumus
@@ -1141,8 +1142,8 @@ pulls=Izmaiņu pieprasījumi
 project_board=Projekti
 packages=Pakotnes
 actions=Darbības
-labels=Etiķetes
-org_labels_desc=Organizācijas līmeņa etiķetes var tikt izmantotas <strong>visiem repozitorijiem</strong> šajā organizācijā
+labels=Iezīmes
+org_labels_desc=Organizācijas līmeņa iezīmes var tikt izmantotas <strong>visiem repozitorijiem</strong> šajā organizācijā
 org_labels_desc_manage=pārvaldīt
 
 milestones=Atskaites punkti
@@ -1334,19 +1335,19 @@ issues.desc=Organizēt kļūdu ziņojumus, uzdevumus un atskaites punktus.
 issues.filter_assignees=Filtrēt pēc atbildīgajiem
 issues.filter_milestones=Filtrēt pēc atskaites punkta
 issues.filter_projects=Filtrēt pēc projekta
-issues.filter_labels=Filtrēt pēc etiķetēm
+issues.filter_labels=Filtrēt pēc iezīmēm
 issues.filter_reviewers=Filtrēt pēc recenzentiem
 issues.new=Jauna problēma
 issues.new.title_empty=Nosaukums nevar būt tukšs
-issues.new.labels=Etiķetes
-issues.new.no_label=Nav etiķešu
-issues.new.clear_labels=Noņemt etiķetes
+issues.new.labels=Iezīmes
+issues.new.no_label=Nav iezīmju
+issues.new.clear_labels=Noņemt iezīmes
 issues.new.projects=Projekti
 issues.new.clear_projects=Notīrīt projektus
 issues.new.no_projects=Nav projektu
 issues.new.open_projects=Aktīvie projekti
 issues.new.closed_projects=Pabeigtie projekti
-issues.new.no_items=Nav neviena ieraksta
+issues.new.no_items=Nav vienumu
 issues.new.milestone=Atskaites punkts
 issues.new.no_milestone=Nav atskaites punktu
 issues.new.clear_milestone=Notīrīt atskaites punktus
@@ -1365,20 +1366,20 @@ issues.choose.invalid_templates=%v ķļūdaina sagatave(s) atrastas
 issues.choose.invalid_config=Problēmu konfigurācija satur kļūdas:
 issues.no_ref=Nav norādīts atzars/tags
 issues.create=Pieteikt problēmu
-issues.new_label=Jauna etiķete
-issues.new_label_placeholder=Etiķetes nosaukums
+issues.new_label=Jauna iezīme
+issues.new_label_placeholder=Iezīmes nosaukums
 issues.new_label_desc_placeholder=Apraksts
-issues.create_label=Izveidot etiķeti
-issues.label_templates.title=Ielādēt sākotnēji noteiktu etiķešu kopu
-issues.label_templates.info=Nav izveidota neviena etiķete. Jūs varat noklikšķināt uz "Jauna etiķete" augstāk, lai to izveidotu vai izmantot zemāk piedāvātās etiķetes:
-issues.label_templates.helper=Izvēlieties etiķešu kopu
-issues.label_templates.use=Izmantot etiķešu kopu
-issues.label_templates.fail_to_load_file=Neizdevās ielādēt etiķetes sagataves failu "%s": %v
-issues.add_label=pievienoja %s etiķeti %s
-issues.add_labels=pievienoja %s etiķetes %s
-issues.remove_label=noņēma %s etiķeti %s
-issues.remove_labels=noņēma %s etiķetes %s
-issues.add_remove_labels=pievienoja %s un noņēma %s etiķetes %s
+issues.create_label=Izveidot iezīmi
+issues.label_templates.title=Ielādēt sākotnēji noteiktu iezīmju kopu
+issues.label_templates.info=Nav izveidota neviena iezīme. Nospiediet uz pogas "Jauna iezīme", lai to izveidotu vai izmantojiet zemāk piedāvātās iezīmju kopas:
+issues.label_templates.helper=Izvēlieties iezīmju kopu
+issues.label_templates.use=Izmantot iezīmju kopu
+issues.label_templates.fail_to_load_file=Neizdevās ielādēt iezīmju sagataves failu "%s": %v
+issues.add_label=pievienoja %s iezīmi %s
+issues.add_labels=pievienoja %s iezīmes %s
+issues.remove_label=noņēma %s iezīmi %s
+issues.remove_labels=noņēma %s iezīmes %s
+issues.add_remove_labels=pievienoja %s un noņēma %s iezīmes %s
 issues.add_milestone_at=`pievienoja atskaites punktu <b>%s</b> %s`
 issues.add_project_at=`pievienoja šo problēmu <b>%s</b> projektam %s`
 issues.change_milestone_at=`nomainīja atskaites punktu no <b>%s</b> uz <b>%s</b> %s`
@@ -1396,9 +1397,9 @@ issues.change_ref_at=`nomainīta atsauce no <b><strike>%s</strike></b> uz <b>%s<
 issues.remove_ref_at=`noņēma atsauci no <b>%s</b> %s`
 issues.add_ref_at=`pievienoja atsauci uz <b>%s</b> %s`
 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=Iezīme
+issues.filter_label_exclude=`Izmantojiet <code>alt</code> + <code>peles klikšķis vai enter</code>, lai neiekļautu iezīmes`
+issues.filter_label_no_select=Visas iezīmes
 issues.filter_label_select_no_label=Nav etiķetes
 issues.filter_milestone=Atskaites punkts
 issues.filter_milestone_all=Visi atskaites punkti
@@ -1435,13 +1436,13 @@ issues.filter_sort.mostforks=Visvairāk atdalītie
 issues.filter_sort.fewestforks=Vismazāk atdalītie
 issues.action_open=Atvērt
 issues.action_close=Aizvērt
-issues.action_label=Etiķete
+issues.action_label=Iezīme
 issues.action_milestone=Atskaites punkts
 issues.action_milestone_no_select=Nav atskaites punkta
 issues.action_assignee=Atbildīgais
 issues.action_assignee_no_select=Nav atbildīgā
 issues.action_check=Atzīmēt/Notīrīt
-issues.action_check_all=Atzīmēt/Notīrīt visus ierakstus
+issues.action_check_all=Atzīmēt/notīrīt visus vienumus
 issues.opened_by=<a href="%[2]s">%[3]s</a> atvēra %[1]s
 pulls.merged_by=<a href="%[2]s">%[3]s</a> sapludināja %[1]s
 pulls.merged_by_fake=%[2]s sapludināja %[1]s
@@ -1502,23 +1503,23 @@ issues.sign_in_require_desc=Nepieciešams <a href="%s">pieteikties</a>, lai piev
 issues.edit=Labot
 issues.cancel=Atcelt
 issues.save=Saglabāt
-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_title=Nosaukums
+issues.label_description=Apraksts
+issues.label_color=Krāsa
+issues.label_exclusive=Sevišķa
 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
+issues.label_exclusive_desc=Nosauciet iezīmi <code>grupa/nosaukums</code>, lai tās grupētu un varētu padarīt kā savstarpēji sevišķas ar citām <code>grupa/</code> iezīmēm.
+issues.label_exclusive_warning=Jebkura konfliktējoša savstarpēji sevišķas grupas iezīme tiks noņemta, labojot problēmas vai izmaiņu pietikuma iezīmes.
+issues.label_count=%d iezīmes
 issues.label_open_issues=%d atvērtas problēmas
 issues.label_edit=Labot
 issues.label_delete=Dzēst
-issues.label_modify=Labot etiķeti
+issues.label_modify=Labot iezīmi
 issues.label_deletion=Dzēst etiķeti
-issues.label_deletion_desc=Dzēšot etiķeti, tā tiks noņemta no visām problēmām un izmaiņu pieprasījumiem. Vai turpināt?
-issues.label_deletion_success=Etiķete tika izdzēsta.
+issues.label_deletion_desc=Dzēšot iezīmi, tā tiks noņemta no visām problēmām un izmaiņu pieprasījumiem. Vai turpināt?
+issues.label_deletion_success=Iezīme tika izdzēsta.
 issues.label.filter_sort.alphabetically=Alfabētiski
 issues.label.filter_sort.reverse_alphabetically=Pretēji alfabētiski
 issues.label.filter_sort.by_size=Mazākais izmērs
@@ -1676,7 +1677,7 @@ pulls.allow_edits_from_maintainers_err=Atjaunošana neizdevās
 pulls.compare_changes_desc=Izvēlieties atzaru, kurā sapludināt izmaiņas un atzaru, no kura tās saņemt.
 pulls.has_viewed_file=Skatīts
 pulls.has_changed_since_last_review=Mainīts kopš pēdējās recenzijas
-pulls.viewed_files_label=%[1]d no %[2]d failiem apskatīts
+pulls.viewed_files_label=apskatīts %[1]d no %[2]d failiem
 pulls.expand_files=Izvērst visus failus
 pulls.collapse_files=Savērst visus failus
 pulls.compare_base=pamata
@@ -1886,7 +1887,7 @@ wiki.page_name_desc=Ievadiet vikivietnes lapas nosaukumu. Speciālie nosaukumi i
 wiki.original_git_entry_tooltip=Attēlot oriģinālo Git faila nosaukumu.
 
 activity=Aktivitāte
-activity.period.filter_label=Laika periods:
+activity.period.filter_label=Laika posms:
 activity.period.daily=1 diena
 activity.period.halfweekly=3 dienas
 activity.period.weekly=1 nedēļa
@@ -2171,8 +2172,8 @@ settings.event_issues=Problēmas
 settings.event_issues_desc=Problēma atvērta, aizvērta, atkārtoti atvērta vai mainīta.
 settings.event_issue_assign=Problēmas atbildīgie
 settings.event_issue_assign_desc=Problēmai piešķirti vai noņemti atbildīgie.
-settings.event_issue_label=Problēmu etiķetes
-settings.event_issue_label_desc=Problēmai pievienotas vai noņemtas etiķetes.
+settings.event_issue_label=Problēmu iezīmes
+settings.event_issue_label_desc=Problēmai pievienotas vai noņemtas iezīmes.
 settings.event_issue_milestone=Problēmas atskaites punkts
 settings.event_issue_milestone_desc=Problēmai pievienots vai noņemts atskaites punkts.
 settings.event_issue_comment=Problēmas komentārs
@@ -2182,8 +2183,8 @@ settings.event_pull_request=Izmaiņu pieprasījums
 settings.event_pull_request_desc=Izmaiņu pieprasījums atvērts, aizvērts, atkārtoti atvērts vai mainīts.
 settings.event_pull_request_assign=Izmaiņu pieprasījuma atbildīgie
 settings.event_pull_request_assign_desc=Izmaiņu pieprasījumam piešķirti vai noņemti atbildīgie.
-settings.event_pull_request_label=Izmaiņu pieprasījuma etiķetes
-settings.event_pull_request_label_desc=Izmaiņu pieprasījumam pievienotas vai noņemtas etiķetes.
+settings.event_pull_request_label=Izmaiņu pieprasījuma iezīmes
+settings.event_pull_request_label_desc=Izmaiņu pieprasījumam tika pievienotas vai noņemtas iezīmes.
 settings.event_pull_request_milestone=Izmaiņu pieprasījuma atskaites punkts
 settings.event_pull_request_milestone_desc=Izmaiņu pieprasījumam pievienots vai noņemts atskaites punkts.
 settings.event_pull_request_comment=Izmaiņu pieprasījuma komentārs
@@ -2598,7 +2599,7 @@ settings.delete_org_title=Dzēst organizāciju
 settings.delete_org_desc=Organizācija tiks dzēsta neatgriezeniski. Vai turpināt?
 settings.hooks_desc=Pievienot tīmekļa āķus, kas nostrādās <strong>visiem repozitorijiem</strong> šajā organizācijā.
 
-settings.labels_desc=Pievienojiet etiķetes, kas var tikt izmantotas <strong>visos</strong> šīs organizācijas repozitorijos.
+settings.labels_desc=Pievienojiet iezīmes, kas var tikt izmantotas <strong>visos</strong> šīs organizācijas repozitorijos.
 
 members.membership_visibility=Dalībnieka redzamība:
 members.public=Redzams
@@ -3217,7 +3218,6 @@ mirror_sync_create=ar spoguli sinhronizēta jauna atsauce <a href="%[2]s">%[3]s<
 mirror_sync_delete=ar spoguli sinhronizēta un izdzēsta atsauce <code>%[2]s</code> repozitorijam <a href="%[1]s">%[3]s</a>
 approve_pull_request=`apstiprināja izmaiņu pieprasījumu <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`ieteica izmaiņas izmaiņu pieprasījumam <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`izveidoja versiju <a href="%[2]s"> "%[4]s" </a> repozitorijā <a href="%[1]s">%[3]s</a>`
 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>
@@ -3337,7 +3337,7 @@ container.pull=Atgādājiet šo attēlu no komandrindas:
 container.digest=Īssavilkums:
 container.multi_arch=OS / arhitektūra
 container.layers=Attēla slāņi
-container.labels=Etiķetes
+container.labels=Iezīmes
 container.labels.key=Atslēga
 container.labels.value=Vērtība
 cran.registry=Iestaties šo reģistru savā <code>Rprofile.site</code> failā:
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 2e23cde801..4799727d98 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -3153,7 +3153,6 @@ mirror_sync_create=sincronizou a nova referência <a href="%[2]s">%[3]s</a> para
 mirror_sync_delete=referência excluída e sincronizada <code>%[2]s</code> em <a href="%[1]s">%[3]s</a> do espelhamento
 approve_pull_request=`aprovou <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`sugeriu modificações para <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`lançou a versão <a href="%[2]s"> "%[4]s" </a> em <a href="%[1]s">%[3]s</a>`
 review_dismissed=`descartou a revisão de <b>%[4]s</b> para <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Motivo:
 create_branch=criou o branch <a href="%[2]s">%[3]s</a> em <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 642d8915cf..f4c77e4981 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -3348,7 +3348,6 @@ mirror_sync_create=sincronizou a nova referência <a href="%[2]s">%[3]s</a> para
 mirror_sync_delete=sincronizou e eliminou a referência <code>%[2]s</code> em <a href="%[1]s">%[3]s</a> da réplica
 approve_pull_request=`aprovou <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`sugeriu modificações para <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`lançou <a href="%[2]s"> "%[4]s" </a> em <a href="%[1]s">%[3]s</a>`
 review_dismissed=`descartou a revisão de <b>%[4]s</b> para <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Motivo:
 create_branch=criou o ramo <a href="%[2]s">%[3]s</a> em <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index df6df4cf95..81b88dbd45 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -3147,7 +3147,6 @@ mirror_sync_create=синхронизировал(а) новую ссылку <a
 mirror_sync_delete=синхронизированные и удалённые ссылки <code>%[2]s</code> на <a href="%[1]s">%[3]s</a> из зеркала
 approve_pull_request=`утвердил(а) задачу <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`предложил(а) изменения для <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`выпустил(а) <a href="%[2]s"> "%[4]s" </a> в <a href="%[1]s">%[3]s</a>`
 review_dismissed=`отклонил(а) отзыв от <b>%[4]s</b> для <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Причина:
 create_branch=создал(а) ветку <a href="%[2]s">%[3]s</a> в <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 15bbcfebb2..cb437e5530 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -2465,7 +2465,6 @@ mirror_sync_create=සමමුහුර්ත නව යොමු <a href="%[2]
 mirror_sync_delete=සමමුහුර්ත සහ මකාදැමූ යොමු <code>%[2]s</code> හි <a href="%[1]s">%[3]s</a> කැඩපතෙන්
 approve_pull_request=`අනුමත <a href="%[1]s">%[3]s #%[2]s ගේ</a>`
 reject_pull_request=<a href="%[1]s">%[3]s #%[2]s</a>සඳහා යෝජිත වෙනස්කම්
-publish_release=`නිදහස් <a href="%[2]s"> "%[4]s" </a> හි <a href="%[1]s">%[3]s</a>`
 review_dismissed_reason=හේතුව:
 create_branch=නිර්මාණය කරන ලද ශාඛාව <a href="%[2]s">%[3]s</a> <a href="%[1]s">%[4]s</a>
 watched_repo=<a href="%[1]s">%[2]s</a>නැරඹීමට පටන් ගත්තා
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index be89113f0d..7b57e416f7 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -3344,7 +3344,6 @@ mirror_sync_create=<a href="%[2]s">%[3]s</a> yeni referansını, <a href="%[1]s"
 mirror_sync_delete=<a href="%[1]s">%[3]s</a> adresindeki <code>%[2]s</code> referansını eşitledi ve sildi
 approve_pull_request=`<a href="%[1]s">%[3]s#%[2]s</a> değişiklik isteğini onayladı`
 reject_pull_request=`<a href="%[1]s">%[3]s#%[2]s</a> için değişiklikler önerdi`
-publish_release=`<a href="%[1]s">%[3]s</a> deposu için <a href="%[2]s"> "%[4]s" </a> sürümü yayınlandı`
 review_dismissed=`<a href="%[1]s">%[3]s#%[2]s</a> için <b>%[4]s</b> yorumunu reddetti`
 review_dismissed_reason=Sebep:
 create_branch=<a href="%[1]s">%[4]s</a> deposunda <a href="%[2]s">%[3]s</a> dalını oluşturdu
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 3e38973e02..ddd884e113 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -2517,7 +2517,6 @@ mirror_sync_create=синхронізував нове посилання <a hre
 mirror_sync_delete=синхронізовано й видалено посилання <code>%[2]s</code> на <a href="%[1]s">%[3]s</a> із дзеркала
 approve_pull_request=`схвалив <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`запропонував зміни до <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`опублікував випуск <a href="%[2]s"> "%[4]s" </a> з <a href="%[1]s">%[3]s</a>`
 review_dismissed=`відхилив відгук від <b>%[4]s</b> для <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Причина:
 create_branch=створив гілку <a href="%[2]s">%[3]s</a> в <a href="%[1]s">%[4]s</a>
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index c98af46d45..10abf90ed7 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -3347,7 +3347,6 @@ mirror_sync_create=从镜像同步了引用 <a href="%[2]s">%[3]s</a> 至仓库
 mirror_sync_delete=从镜像同步并从 <a href="%[1]s">%[3]s</a> 删除了引用 <code>%[2]s</code>
 approve_pull_request=`批准了 <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`建议变更 <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`在 <a href="%[1]s">%[3]s</a> 发布了 <a href="%[2]s"> "%[4]s" </a>`
 review_dismissed=`取消了 <b>%[4]s</b> 对 <a href="%[1]s">%[3]s#%[2]s</a> 的变更请求`
 review_dismissed_reason=原因:
 create_branch=于 <a href="%[1]s">%[4]s</a> 创建了分支 <a href="%[2]s">%[3]s</a>
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 7823426990..50c0276567 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -2932,7 +2932,6 @@ mirror_sync_create=從鏡像同步了新參考 <a href="%[2]s">%[3]s</a> 到 <a
 mirror_sync_delete=從鏡像同步並從 <a href="%[1]s">%[3]s</a> 刪除了參考 <code>%[2]s</code>
 approve_pull_request=`核可了 <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`提出了修改建議 <a href="%[1]s">%[3]s#%[2]s</a>`
-publish_release=`發布了 <a href="%[1]s">%[3]s</a> 的 <a href="%[2]s"> "%[4]s" </a>`
 review_dismissed=`取消了 <b>%[4]s</b> 對 <a href="%[1]s">%[3]s#%[2]s</a> 的審核`
 review_dismissed_reason=原因:
 create_branch=在 <a href="%[1]s">%[4]s</a> 中建立了分支 <a href="%[2]s">%[3]s</a>

From d0d6aad85f4d1e2a6d2a6524fe13eccecfd350af Mon Sep 17 00:00:00 2001
From: dicarne <dicarne@zhishudali.ink>
Date: Wed, 15 May 2024 21:56:17 +0800
Subject: [PATCH 004/131] Supports forced use of S3 virtual-hosted style
 (#30969)

Add a configuration item to enable S3 virtual-hosted style (V2) to solve
the problem caused by some S3 service providers not supporting path
style (V1).
---
 cmd/migrate_storage.go                        |  6 ++++++
 custom/conf/app.example.ini                   |  6 ++++++
 .../config-cheat-sheet.en-us.md               |  8 ++++++++
 .../config-cheat-sheet.zh-cn.md               |  8 ++++++++
 modules/setting/storage.go                    |  2 ++
 modules/storage/minio.go                      | 20 +++++++++++++++----
 6 files changed, 46 insertions(+), 4 deletions(-)

diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go
index 357416fc33..7d1ef052ff 100644
--- a/cmd/migrate_storage.go
+++ b/cmd/migrate_storage.go
@@ -91,6 +91,11 @@ var CmdMigrateStorage = &cli.Command{
 			Value: "",
 			Usage: "Minio checksum algorithm (default/md5)",
 		},
+		&cli.StringFlag{
+			Name:  "minio-bucket-lookup-type",
+			Value: "",
+			Usage: "Minio bucket lookup type",
+		},
 	},
 }
 
@@ -220,6 +225,7 @@ func runMigrateStorage(ctx *cli.Context) error {
 					UseSSL:             ctx.Bool("minio-use-ssl"),
 					InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"),
 					ChecksumAlgorithm:  ctx.String("minio-checksum-algorithm"),
+					BucketLookUpType:   ctx.String("minio-bucket-lookup-type"),
 				},
 			})
 	default:
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 577479e39f..4df843b8ce 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1895,6 +1895,9 @@ LEVEL = Info
 ;;
 ;; Minio checksum algorithm: default (for MinIO or AWS S3) or md5 (for Cloudflare or Backblaze)
 ;MINIO_CHECKSUM_ALGORITHM = default
+;;
+;; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
+;MINIO_BUCKET_LOOKUP_TYPE = auto
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2576,6 +2579,9 @@ LEVEL = Info
 ;;
 ;; Minio skip SSL verification available when STORAGE_TYPE is `minio`
 ;MINIO_INSECURE_SKIP_VERIFY = false
+;;
+;; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
+;MINIO_BUCKET_LOOKUP_TYPE = auto
 
 ;[proxy]
 ;; Enable the proxy, all requests to external via HTTP will be affected
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 07712c1110..6c429bb652 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -851,6 +851,7 @@ Default templates for project boards:
 - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when STORAGE_TYPE is `minio`
 - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
 - `MINIO_CHECKSUM_ALGORITHM`: **default**: Minio checksum algorithm: `default` (for MinIO or AWS S3) or `md5` (for Cloudflare or Backblaze)
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
 
 ## Log (`log`)
 
@@ -1272,6 +1273,7 @@ is `data/lfs` and the default of `MINIO_BASE_PATH` is `lfs/`.
 - `MINIO_BASE_PATH`: **lfs/**: Minio base path on the bucket only available when `STORAGE_TYPE` is `minio`
 - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
 - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
 
 ## Storage (`storage`)
 
@@ -1286,6 +1288,7 @@ Default storage configuration for attachments, lfs, avatars, repo-avatars, repo-
 - `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
 - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
 - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
 
 The recommended storage configuration for minio like below:
 
@@ -1307,6 +1310,8 @@ MINIO_USE_SSL = false
 ; Minio skip SSL verification available when STORAGE_TYPE is `minio`
 MINIO_INSECURE_SKIP_VERIFY = false
 SERVE_DIRECT = true
+; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
+MINIO_BUCKET_LOOKUP_TYPE = auto
 ```
 
 Defaultly every storage has their default base path like below
@@ -1353,6 +1358,8 @@ MINIO_LOCATION = us-east-1
 MINIO_USE_SSL = false
 ; Minio skip SSL verification available when STORAGE_TYPE is `minio`
 MINIO_INSECURE_SKIP_VERIFY = false
+; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
+MINIO_BUCKET_LOOKUP_TYPE = auto
 ```
 
 ## Repository Archive Storage (`storage.repo-archive`)
@@ -1372,6 +1379,7 @@ is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`.
 - `MINIO_BASE_PATH`: **repo-archive/**: Minio base path on the bucket only available when `STORAGE_TYPE` is `minio`
 - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
 - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
 
 ## Repository Archives (`repo-archive`)
 
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 3bb31d3d71..3c6ac8c00a 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -796,6 +796,7 @@ Gitea 创建以下非唯一队列:
 - `MINIO_USE_SSL`: **false**: Minio 启用 SSL,仅当 STORAGE_TYPE 为 `minio` 时可用。
 - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio 跳过 SSL 验证,仅当 STORAGE_TYPE 为 `minio` 时可用。
 - `MINIO_CHECKSUM_ALGORITHM`: **default**: Minio 校验算法:`default`(适用于 MinIO 或 AWS S3)或 `md5`(适用于 Cloudflare 或 Backblaze)
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio的bucket查找方式默认为`auto`模式,可将其设置为`dns`(虚拟托管样式)或`path`(路径样式),仅当`STORAGE_TYPE`为`minio`时可用。
 
 ## 日志 (`log`)
 
@@ -1201,6 +1202,7 @@ ALLOW_DATA_URI_IMAGES = true
 - `MINIO_BASE_PATH`:**lfs/**:桶上的 Minio 基本路径,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
 - `MINIO_USE_SSL`:**false**:Minio 启用 ssl,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
 - `MINIO_INSECURE_SKIP_VERIFY`:**false**:Minio 跳过 SSL 验证,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio的bucket查找方式默认为`auto`模式,可将其设置为`dns`(虚拟托管样式)或`path`(路径样式),仅当`STORAGE_TYPE`为`minio`时可用。
 
 ## 存储 (`storage`)
 
@@ -1215,6 +1217,7 @@ ALLOW_DATA_URI_IMAGES = true
 - `MINIO_LOCATION`:**us-east-1**:创建桶的 Minio 位置,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
 - `MINIO_USE_SSL`:**false**:Minio 启用 ssl,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
 - `MINIO_INSECURE_SKIP_VERIFY`:**false**:Minio 跳过 SSL 验证,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio的bucket查找方式默认为`auto`模式,可将其设置为`dns`(虚拟托管样式)或`path`(路径样式),仅当`STORAGE_TYPE`为`minio`时可用。
 
 建议的 minio 存储配置如下:
 
@@ -1236,6 +1239,8 @@ MINIO_USE_SSL = false
 ; Minio skip SSL verification available when STORAGE_TYPE is `minio`
 MINIO_INSECURE_SKIP_VERIFY = false
 SERVE_DIRECT = true
+; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
+MINIO_BUCKET_LOOKUP_TYPE = auto
 ```
 
 默认情况下,每个存储都有其默认的基本路径,如下所示:
@@ -1282,6 +1287,8 @@ MINIO_LOCATION = us-east-1
 MINIO_USE_SSL = false
 ; Minio skip SSL verification available when STORAGE_TYPE is `minio`
 MINIO_INSECURE_SKIP_VERIFY = false
+; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
+MINIO_BUCKET_LOOKUP_TYPE = auto
 ```
 
 ### 存储库归档存储 (`storage.repo-archive`)
@@ -1299,6 +1306,7 @@ MINIO_INSECURE_SKIP_VERIFY = false
 - `MINIO_BASE_PATH`: **repo-archive/**:存储桶上的Minio基本路径,仅在`STORAGE_TYPE`为`minio`时可用。
 - `MINIO_USE_SSL`: **false**:启用Minio的SSL,仅在`STORAGE_TYPE`为`minio`时可用。
 - `MINIO_INSECURE_SKIP_VERIFY`: **false**:跳过Minio的SSL验证,仅在`STORAGE_TYPE`为`minio`时可用。
+- `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio的bucket查找方式默认为`auto`模式,可将其设置为`dns`(虚拟托管样式)或`path`(路径样式),仅当`STORAGE_TYPE`为`minio`时可用。
 
 ### 存储库归档 (`repo-archive`)
 
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index 0bd52acc0f..d80a61a45e 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -47,6 +47,7 @@ type MinioStorageConfig struct {
 	InsecureSkipVerify bool   `ini:"MINIO_INSECURE_SKIP_VERIFY"`
 	ChecksumAlgorithm  string `ini:"MINIO_CHECKSUM_ALGORITHM" json:",omitempty"`
 	ServeDirect        bool   `ini:"SERVE_DIRECT"`
+	BucketLookUpType   string `ini:"MINIO_BUCKET_LOOKUP_TYPE" json:",omitempty"`
 }
 
 // Storage represents configuration of storages
@@ -82,6 +83,7 @@ func getDefaultStorageSection(rootCfg ConfigProvider) ConfigSection {
 	storageSec.Key("MINIO_USE_SSL").MustBool(false)
 	storageSec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false)
 	storageSec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default")
+	storageSec.Key("MINIO_BUCKET_LOOKUP_TYPE").MustString("auto")
 	return storageSec
 }
 
diff --git a/modules/storage/minio.go b/modules/storage/minio.go
index b58ab67dc7..986332dfed 100644
--- a/modules/storage/minio.go
+++ b/modules/storage/minio.go
@@ -85,11 +85,23 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
 
 	log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
 
+	var lookup minio.BucketLookupType
+	if config.BucketLookUpType == "auto" || config.BucketLookUpType == "" {
+		lookup = minio.BucketLookupAuto
+	} else if config.BucketLookUpType == "dns" {
+		lookup = minio.BucketLookupDNS
+	} else if config.BucketLookUpType == "path" {
+		lookup = minio.BucketLookupPath
+	} else {
+		return nil, fmt.Errorf("invalid minio bucket lookup type: %s", config.BucketLookUpType)
+	}
+
 	minioClient, err := minio.New(config.Endpoint, &minio.Options{
-		Creds:     credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
-		Secure:    config.UseSSL,
-		Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
-		Region:    config.Location,
+		Creds:        credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
+		Secure:       config.UseSSL,
+		Transport:    &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
+		Region:       config.Location,
+		BucketLookup: lookup,
 	})
 	if err != nil {
 		return nil, convertMinioErr(err)

From fc89363832c87678d9e35e865b49c63c7ad498f2 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Wed, 15 May 2024 22:25:47 +0800
Subject: [PATCH 005/131] Check if the release is converted from the tag when
 updating the release (#30984)

Call `notify_service.NewRelease` when a release is created
from an existing tag.
---
 services/release/release.go | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/services/release/release.go b/services/release/release.go
index ba5fd1dd98..399fdc79c0 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -204,7 +204,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
 	if rel.ID == 0 {
 		return errors.New("UpdateRelease only accepts an exist release")
 	}
-	isCreated, err := createTag(gitRepo.Ctx, gitRepo, rel, "")
+	isTagCreated, err := createTag(gitRepo.Ctx, gitRepo, rel, "")
 	if err != nil {
 		return err
 	}
@@ -216,6 +216,12 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
 	}
 	defer committer.Close()
 
+	oldRelease, err := repo_model.GetReleaseByID(ctx, rel.ID)
+	if err != nil {
+		return err
+	}
+	isConvertedFromTag := oldRelease.IsTag && !rel.IsTag
+
 	if err = repo_model.UpdateRelease(ctx, rel); err != nil {
 		return err
 	}
@@ -292,7 +298,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
 	}
 
 	if !rel.IsDraft {
-		if !isCreated {
+		if !isTagCreated && !isConvertedFromTag {
 			notify_service.UpdateRelease(gitRepo.Ctx, doer, rel)
 			return nil
 		}

From ea8e4baacc5c58e45e68291334c3d2c42e9d6737 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 15 May 2024 16:54:34 +0200
Subject: [PATCH 006/131] Put web editor into a segment (#30966)

Implement
https://github.com/go-gitea/gitea/pull/30707#issuecomment-2084126206

Diff without whitespace:
https://github.com/go-gitea/gitea/pull/30966/files?diff=unified&w=1

Might as well backport.
---
 templates/repo/editor/edit.tmpl | 42 ++++++++++++++++++---------------
 1 file changed, 23 insertions(+), 19 deletions(-)

diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl
index ae3f12669c..e990177d8a 100644
--- a/templates/repo/editor/edit.tmpl
+++ b/templates/repo/editor/edit.tmpl
@@ -26,26 +26,30 @@
 				</div>
 			</div>
 			<div class="field">
-				<div class="ui compact small menu small-menu-items repo-editor-menu">
-					<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" 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 class="ui top attached header">
+					<div class="ui compact small menu small-menu-items repo-editor-menu">
+						<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" 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>
-				<div class="ui active tab segment tw-rounded" data-tab="write">
-					<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}}"
-						data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
-					<div class="editor-loading is-loading"></div>
-				</div>
-				<div class="ui tab segment markup tw-rounded" data-tab="preview">
-					{{ctx.Locale.Tr "loading"}}
-				</div>
-				<div class="ui tab segment diff edit-diff" data-tab="diff">
-					<div class="tw-p-16"></div>
+				<div class="ui bottom attached segment tw-p-0">
+					<div class="ui active tab tw-rounded" data-tab="write">
+						<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}}"
+							data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
+						<div class="editor-loading is-loading"></div>
+					</div>
+					<div class="ui tab markup tw-px-4 tw-py-3" data-tab="preview">
+						{{ctx.Locale.Tr "loading"}}
+					</div>
+					<div class="ui tab diff edit-diff" data-tab="diff">
+						<div class="tw-p-16"></div>
+					</div>
 				</div>
 			</div>
 			{{template "repo/editor/commit_form" .}}

From 2611249511aaab710ad7b0bccd049d3e4dc912f4 Mon Sep 17 00:00:00 2001
From: Frank Villaro-Dixon <frank@vi-di.fr>
Date: Thu, 16 May 2024 08:36:31 +0200
Subject: [PATCH 007/131] template: `label` fix correct input id (#30987)

Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
---
 templates/repo/settings/deploy_keys.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index da1a321785..190ca1af6c 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -28,7 +28,7 @@
 					<div class="field">
 						<div class="ui checkbox {{if .Err_IsWritable}}error{{end}}">
 							<input id="ssh-key-is-writable" name="is_writable" type="checkbox" value="1">
-							<label for="is_writable">
+							<label for="ssh-key-is-writable">
 								{{ctx.Locale.Tr "repo.settings.is_writable"}}
 							</label>
 							<small class="tw-pl-[26px]">{{ctx.Locale.Tr "repo.settings.is_writable_info"}}</small>

From 740b6e1389911eeea860cfccd4bad218fe33f3bd Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 16 May 2024 21:04:25 +0800
Subject: [PATCH 008/131] Fix JS error when editing a merged PR's title
 (#30990)

---
 templates/repo/issue/view_title.tmpl | 6 ++----
 web_src/js/features/repo-issue.js    | 5 ++++-
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 4415ad79f5..097d7b1f7c 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -26,9 +26,7 @@
 		</div>
 		<div class="issue-title-buttons">
 			<button class="ui small basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
-			<button class="ui small primary button"
-							data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title"
-							{{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>
+			<button class="ui small primary button" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title">
 				{{ctx.Locale.Tr "repo.issues.save"}}
 			</button>
 		</div>
@@ -77,7 +75,7 @@
 							{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}
 						</span>
 					{{end}}
-					<span id="pull-desc-editor" class="tw-hidden flex-text-block">
+					<span id="pull-desc-editor" class="tw-hidden flex-text-block" data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch">
 						<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/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 8ee681aedc..519db34934 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -626,9 +626,12 @@ export function initRepoIssueTitleEdit() {
     showElem(issueTitleDisplay);
     showElem('#pull-desc-display');
   });
+
+  const pullDescEditor = document.querySelector('#pull-desc-editor'); // it may not exist for a merged PR
+  const prTargetUpdateUrl = pullDescEditor?.getAttribute('data-target-update-url');
+
   const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
   editSaveButton.addEventListener('click', async () => {
-    const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url');
     const newTitle = issueTitleInput.value.trim();
     try {
       if (newTitle && newTitle !== oldTitle) {

From a73e3c6a696029541ebd423f4eb2fec1ba151f79 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 16 May 2024 21:40:57 +0200
Subject: [PATCH 009/131] Upgrade `tqdm` dependency (#30996)

Result of `make update-py`

Fixes: https://github.com/go-gitea/gitea/security/dependabot/65
---
 poetry.lock | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index 1533ddc5ec..74536495d2 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
 
 [[package]]
 name = "click"
@@ -318,13 +318,13 @@ files = [
 
 [[package]]
 name = "tqdm"
-version = "4.66.2"
+version = "4.66.4"
 description = "Fast, Extensible Progress Meter"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
-    {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
+    {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
+    {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
 ]
 
 [package.dependencies]

From 68d5c18953620927101609bbd21508213cbcd589 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Fri, 17 May 2024 00:25:42 +0000
Subject: [PATCH 010/131] [skip ci] Updated translations via Crowdin

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

diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 10abf90ed7..0e224f0061 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -3320,6 +3320,7 @@ self_check.database_collation_case_insensitive=数据库正在使用一个校验
 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手动解决这个问题。
+self_check.location_origin_mismatch=当前 URL (%[1]s) 与 Gitea 的 URL (%[2]s) 不匹配 。 如果您正在使用反向代理,请确保设置正确的“主机”和“X-转发-原始”标题。
 
 [action]
 create_repo=创建了仓库 <a href="%s">%s</a>
@@ -3347,6 +3348,7 @@ mirror_sync_create=从镜像同步了引用 <a href="%[2]s">%[3]s</a> 至仓库
 mirror_sync_delete=从镜像同步并从 <a href="%[1]s">%[3]s</a> 删除了引用 <code>%[2]s</code>
 approve_pull_request=`批准了 <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`建议变更 <a href="%[1]s">%[3]s#%[2]s</a>`
+publish_release=`在 <a href="%[1]s">%[3]s</a> 发布了 <a href="%[2]s"> %[4]s </a>`
 review_dismissed=`取消了 <b>%[4]s</b> 对 <a href="%[1]s">%[3]s#%[2]s</a> 的变更请求`
 review_dismissed_reason=原因:
 create_branch=于 <a href="%[1]s">%[4]s</a> 创建了分支 <a href="%[2]s">%[3]s</a>

From 821d2fc2a3cc897f21d707455850177077b72410 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 18 May 2024 00:07:41 +0800
Subject: [PATCH 011/131] Simplify mirror repository API logic (#30963)

Fix #30921
---
 modules/structs/repo.go        |  2 +-
 routers/api/v1/repo/repo.go    | 12 +++---------
 templates/swagger/v1_json.tmpl |  2 +-
 3 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index bc8eb0b756..1fe826cf89 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -217,7 +217,7 @@ type EditRepoOption struct {
 	Archived *bool `json:"archived,omitempty"`
 	// set to a string like `8h30m0s` to set the mirror interval time
 	MirrorInterval *string `json:"mirror_interval,omitempty"`
-	// enable prune - remove obsolete remote-tracking references
+	// enable prune - remove obsolete remote-tracking references when mirroring
 	EnablePrune *bool `json:"enable_prune,omitempty"`
 }
 
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 7f35a7fe41..e759142938 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -1062,16 +1062,10 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
 func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
 	repo := ctx.Repo.Repository
 
-	// only update mirror if interval or enable prune are provided
-	if opts.MirrorInterval == nil && opts.EnablePrune == nil {
-		return nil
-	}
-
-	// these values only make sense if the repo is a mirror
+	// Skip this update if the repo is not a mirror, do not return error.
+	// Because reporting errors only makes the logic more complex&fragile, it doesn't really help end users.
 	if !repo.IsMirror {
-		err := fmt.Errorf("repo is not a mirror, can not change mirror interval")
-		ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
-		return err
+		return nil
 	}
 
 	// get the mirror from the repo
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 9ad0aa2ab6..0b3f5cdcad 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -20753,7 +20753,7 @@
           "x-go-name": "Description"
         },
         "enable_prune": {
-          "description": "enable prune - remove obsolete remote-tracking references",
+          "description": "enable prune - remove obsolete remote-tracking references when mirroring",
           "type": "boolean",
           "x-go-name": "EnablePrune"
         },

From 028992429a2e14de39c9bb028637948e446d23ad Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 18 May 2024 10:53:28 +0200
Subject: [PATCH 012/131] Clean up revive linter config, tweak golangci output
 (#30980)

The `errorCode` and `warningCode` options were removed at some point,
they are not recognized by golangci-lint any more at least and they do
not match their published json schema. `confidence` and
`ignore-generated-header` are at the default value so does not need to
be configured.

https://golangci-lint.run/usage/linters/#revive
---
 .golangci.yml | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/.golangci.yml b/.golangci.yml
index 238f6cb837..1750872765 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -29,6 +29,8 @@ run:
 
 output:
   sort-results: true
+  sort-order: [file]
+  show-stats: true
 
 linters-settings:
   stylecheck:
@@ -40,11 +42,7 @@ linters-settings:
       - ifElseChain
       - singleCaseSwitch # Every time this occurred in the code, there  was no other way.
   revive:
-    ignore-generated-header: false
-    severity: warning
-    confidence: 0.8
-    errorCode: 1
-    warningCode: 1
+    severity: error
     rules:
       - name: atomic
       - name: bare-return

From 58a03e9fadb345de5653345c2a68ecfd0750940a Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sun, 19 May 2024 12:58:39 +0800
Subject: [PATCH 013/131] Fix bug on avatar (#31008)

Co-authored-by: silverwind <me@silverwind.io>
---
 routers/api/v1/org/avatar.go  |  2 ++
 routers/api/v1/user/avatar.go |  2 ++
 services/user/avatar.go       | 32 +++++++++++++++++++++-----------
 3 files changed, 25 insertions(+), 11 deletions(-)

diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go
index e34c68dfc9..f11eb6c1cd 100644
--- a/routers/api/v1/org/avatar.go
+++ b/routers/api/v1/org/avatar.go
@@ -46,6 +46,7 @@ func UpdateAvatar(ctx *context.APIContext) {
 	err = user_service.UploadAvatar(ctx, ctx.Org.Organization.AsUser(), content)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "UploadAvatar", err)
+		return
 	}
 
 	ctx.Status(http.StatusNoContent)
@@ -72,6 +73,7 @@ func DeleteAvatar(ctx *context.APIContext) {
 	err := user_service.DeleteAvatar(ctx, ctx.Org.Organization.AsUser())
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err)
+		return
 	}
 
 	ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go
index f912296228..30ccb63587 100644
--- a/routers/api/v1/user/avatar.go
+++ b/routers/api/v1/user/avatar.go
@@ -39,6 +39,7 @@ func UpdateAvatar(ctx *context.APIContext) {
 	err = user_service.UploadAvatar(ctx, ctx.Doer, content)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "UploadAvatar", err)
+		return
 	}
 
 	ctx.Status(http.StatusNoContent)
@@ -57,6 +58,7 @@ func DeleteAvatar(ctx *context.APIContext) {
 	err := user_service.DeleteAvatar(ctx, ctx.Doer)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err)
+		return
 	}
 
 	ctx.Status(http.StatusNoContent)
diff --git a/services/user/avatar.go b/services/user/avatar.go
index 2d6c3faf9a..3f87466eaa 100644
--- a/services/user/avatar.go
+++ b/services/user/avatar.go
@@ -5,8 +5,10 @@ package user
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
+	"os"
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
@@ -48,16 +50,24 @@ func UploadAvatar(ctx context.Context, u *user_model.User, data []byte) error {
 func DeleteAvatar(ctx context.Context, u *user_model.User) error {
 	aPath := u.CustomAvatarRelativePath()
 	log.Trace("DeleteAvatar[%d]: %s", u.ID, aPath)
-	if len(u.Avatar) > 0 {
-		if err := storage.Avatars.Delete(aPath); err != nil {
-			return fmt.Errorf("Failed to remove %s: %w", aPath, err)
-		}
-	}
 
-	u.UseCustomAvatar = false
-	u.Avatar = ""
-	if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil {
-		return fmt.Errorf("DeleteAvatar: %w", err)
-	}
-	return nil
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		hasAvatar := len(u.Avatar) > 0
+		u.UseCustomAvatar = false
+		u.Avatar = ""
+		if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil {
+			return fmt.Errorf("DeleteAvatar: %w", err)
+		}
+
+		if hasAvatar {
+			if err := storage.Avatars.Delete(aPath); err != nil {
+				if !errors.Is(err, os.ErrNotExist) {
+					return fmt.Errorf("failed to remove %s: %w", aPath, err)
+				}
+				log.Warn("Deleting avatar %s but it doesn't exist", aPath)
+			}
+		}
+
+		return nil
+	})
 }

From 339bc8bc8fdb4ead3c43b4604b100f83e6f47cb5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 19 May 2024 22:56:08 +0800
Subject: [PATCH 014/131] Improve reverse proxy documents and clarify the
 AppURL guessing behavior (#31003)

Fix #31002

1. Mention Make sure `Host` and `X-Fowarded-Proto` headers are correctly passed to Gitea
2. Clarify the basic requirements and move the "general configuration" to the top
3. Add a comment for the "container registry"
4. Use 1.21 behavior if the reverse proxy is not correctly configured

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 .../administration/reverse-proxies.en-us.md   | 92 ++++++++++---------
 modules/httplib/url.go                        | 31 ++++---
 modules/httplib/url_test.go                   | 12 +--
 routers/api/packages/container/container.go   |  2 +
 routers/web/admin/admin_test.go               |  2 +-
 5 files changed, 78 insertions(+), 61 deletions(-)

diff --git a/docs/content/administration/reverse-proxies.en-us.md b/docs/content/administration/reverse-proxies.en-us.md
index fe54c67d02..5fbd0eb0b7 100644
--- a/docs/content/administration/reverse-proxies.en-us.md
+++ b/docs/content/administration/reverse-proxies.en-us.md
@@ -17,15 +17,35 @@ menu:
 
 # Reverse Proxies
 
+## General configuration
+
+1. Set `[server] ROOT_URL = https://git.example.com/` in your `app.ini` file.
+2. Make the reverse-proxy pass `https://git.example.com/foo` to `http://gitea:3000/foo`.
+3. Make sure the reverse-proxy does not decode the URI. The request `https://git.example.com/a%2Fb` should be passed as `http://gitea:3000/a%2Fb`.
+4. Make sure `Host` and `X-Fowarded-Proto` headers are correctly passed to Gitea to make Gitea see the real URL being visited.
+
+### Use a sub-path
+
+Usually it's **not recommended** to put Gitea in a sub-path, it's not widely used and may have some issues in rare cases.
+
+To make Gitea work with a sub-path (eg: `https://common.example.com/gitea/`),
+there are some extra requirements besides the general configuration above:
+
+1. Use `[server] ROOT_URL = https://common.example.com/gitea/` in your `app.ini` file.
+2. Make the reverse-proxy pass `https://common.example.com/gitea/foo` to `http://gitea:3000/foo`.
+3. The container registry requires a fixed sub-path `/v2` at the root level which must be configured:
+   - Make the reverse-proxy pass `https://common.example.com/v2` to `http://gitea:3000/v2`.
+   - Make sure the URI and headers are also correctly passed (see the general configuration above).
+
 ## Nginx
 
-If you want Nginx to serve your Gitea instance, add the following `server` section to the `http` section of `nginx.conf`:
+If you want Nginx to serve your Gitea instance, add the following `server` section to the `http` section of `nginx.conf`.
 
-```
+Make sure `client_max_body_size` is large enough, otherwise there would be "413 Request Entity Too Large" error when uploading large files.
+
+```nginx
 server {
-    listen 80;
-    server_name git.example.com;
-
+    ...
     location / {
         client_max_body_size 512M;
         proxy_pass http://localhost:3000;
@@ -39,37 +59,35 @@ server {
 }
 ```
 
-### Resolving Error: 413 Request Entity Too Large
-
-This error indicates nginx is configured to restrict the file upload size,
-it affects attachment uploading, form posting, package uploading and LFS pushing, etc.
-You can fine tune the `client_max_body_size` option according to [nginx document](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
-
 ## Nginx with a sub-path
 
-In case you already have a site, and you want Gitea to share the domain name, you can setup Nginx to serve Gitea under a sub-path by adding the following `server` section inside the `http` section of `nginx.conf`:
+In case you already have a site, and you want Gitea to share the domain name,
+you can setup Nginx to serve Gitea under a sub-path by adding the following `server` section
+into the `http` section of `nginx.conf`:
 
-```
+```nginx
 server {
-    listen 80;
-    server_name git.example.com;
-
-    # Note: Trailing slash
-    location /gitea/ {
+    ...
+    location ~ ^/(gitea|v2)($|/) {
         client_max_body_size 512M;
 
-        # make nginx use unescaped URI, keep "%2F" as is
+        # make nginx use unescaped URI, keep "%2F" as-is, remove the "/gitea" sub-path prefix, pass "/v2" as-is.
         rewrite ^ $request_uri;
-        rewrite ^/gitea(/.*) $1 break;
+        rewrite ^(/gitea)?(/.*) $2 break;
         proxy_pass http://127.0.0.1:3000$uri;
 
         # other common HTTP headers, see the "Nginx" config section above
-        proxy_set_header ...
+        proxy_set_header Connection $http_connection;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
     }
 }
 ```
 
-Then you **MUST** set something like `[server] ROOT_URL = http://git.example.com/git/` correctly in your configuration.
+Then you **MUST** set something like `[server] ROOT_URL = http://git.example.com/gitea/` correctly in your configuration.
 
 ## Nginx and serve static resources directly
 
@@ -93,7 +111,7 @@ or use a cdn for the static files.
 
 Set `[server] STATIC_URL_PREFIX = /_/static` in your configuration.
 
-```apacheconf
+```nginx
 server {
     listen 80;
     server_name git.example.com;
@@ -112,7 +130,7 @@ server {
 
 Set `[server] STATIC_URL_PREFIX = http://cdn.example.com/gitea` in your configuration.
 
-```apacheconf
+```nginx
 # application server running Gitea
 server {
     listen 80;
@@ -124,7 +142,7 @@ server {
 }
 ```
 
-```apacheconf
+```nginx
 # static content delivery server
 server {
     listen 80;
@@ -151,6 +169,8 @@ If you want Apache HTTPD to serve your Gitea instance, you can add the following
     ProxyRequests off
     AllowEncodedSlashes NoDecode
     ProxyPass / http://localhost:3000/ nocanon
+    ProxyPreserveHost On
+    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
 </VirtualHost>
 ```
 
@@ -172,6 +192,8 @@ In case you already have a site, and you want Gitea to share the domain name, yo
     AllowEncodedSlashes NoDecode
     # Note: no trailing slash after either /git or port
     ProxyPass /git http://localhost:3000 nocanon
+    ProxyPreserveHost On
+    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
 </VirtualHost>
 ```
 
@@ -183,7 +205,7 @@ Note: The following Apache HTTPD mods must be enabled: `proxy`, `proxy_http`.
 
 If you want Caddy to serve your Gitea instance, you can add the following server block to your Caddyfile:
 
-```apacheconf
+```
 git.example.com {
     reverse_proxy localhost:3000
 }
@@ -193,7 +215,7 @@ git.example.com {
 
 In case you already have a site, and you want Gitea to share the domain name, you can setup Caddy to serve Gitea under a sub-path by adding the following to your server block in your Caddyfile:
 
-```apacheconf
+```
 git.example.com {
     route /git/* {
         uri strip_prefix /git
@@ -371,19 +393,3 @@ gitea:
 This config assumes that you are handling HTTPS on the traefik side and using HTTP between Gitea and traefik.
 
 Then you **MUST** set something like `[server] ROOT_URL = http://example.com/gitea/` correctly in your configuration.
-
-## General sub-path configuration
-
-Usually it's not recommended to put Gitea in a sub-path, it's not widely used and may have some issues in rare cases.
-
-If you really need to do so, to make Gitea works with sub-path (eg: `http://example.com/gitea/`), here are the requirements:
-
-1. Set `[server] ROOT_URL = http://example.com/gitea/` in your `app.ini` file.
-2. Make the reverse-proxy pass `http://example.com/gitea/foo` to `http://gitea-server:3000/foo`.
-3. Make sure the reverse-proxy not decode the URI, the request `http://example.com/gitea/a%2Fb` should be passed as `http://gitea-server:3000/a%2Fb`.
-
-## Docker / Container Registry
-
-The container registry uses a fixed sub-path `/v2` which can't be changed.
-Even if you deploy Gitea with a different sub-path, `/v2` will be used by the `docker` client.
-Therefore you may need to add an additional route to your reverse proxy configuration.
diff --git a/modules/httplib/url.go b/modules/httplib/url.go
index 541c4f325b..8dc5b71181 100644
--- a/modules/httplib/url.go
+++ b/modules/httplib/url.go
@@ -32,7 +32,7 @@ func IsRelativeURL(s string) bool {
 	return err == nil && urlIsRelative(s, u)
 }
 
-func guessRequestScheme(req *http.Request, def string) string {
+func getRequestScheme(req *http.Request) string {
 	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
 	if s := req.Header.Get("X-Forwarded-Proto"); s != "" {
 		return s
@@ -49,10 +49,10 @@ func guessRequestScheme(req *http.Request, def string) string {
 	if s := req.Header.Get("X-Forwarded-Ssl"); s != "" {
 		return util.Iif(s == "on", "https", "http")
 	}
-	return def
+	return ""
 }
 
-func guessForwardedHost(req *http.Request) string {
+func getForwardedHost(req *http.Request) string {
 	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
 	return req.Header.Get("X-Forwarded-Host")
 }
@@ -63,15 +63,24 @@ func GuessCurrentAppURL(ctx context.Context) string {
 	if !ok {
 		return setting.AppURL
 	}
-	if host := guessForwardedHost(req); host != "" {
-		// if it is behind a reverse proxy, use "https" as default scheme in case the site admin forgets to set the correct forwarded-protocol headers
-		return guessRequestScheme(req, "https") + "://" + host + setting.AppSubURL + "/"
-	} else if req.Host != "" {
-		// if it is not behind a reverse proxy, use the scheme from config options, meanwhile use "https" as much as possible
-		defaultScheme := util.Iif(setting.Protocol == "http", "http", "https")
-		return guessRequestScheme(req, defaultScheme) + "://" + req.Host + setting.AppSubURL + "/"
+	// If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one.
+	// At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
+	// There are some cases:
+	// 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly.
+	// 2. The reverse proxy is not configured correctly, doesn't pass "X-Forwarded-Proto/Host" headers, eg: only one "proxy_pass http://gitea:3000" in Nginx.
+	// 3. There is no reverse proxy.
+	// Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3,
+	// then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users.
+	// So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
+	reqScheme := getRequestScheme(req)
+	if reqScheme == "" {
+		return setting.AppURL
 	}
-	return setting.AppURL
+	reqHost := getForwardedHost(req)
+	if reqHost == "" {
+		reqHost = req.Host
+	}
+	return reqScheme + "://" + reqHost + setting.AppSubURL + "/"
 }
 
 func MakeAbsoluteURL(ctx context.Context, s string) string {
diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go
index e021cd610d..9980cb74e8 100644
--- a/modules/httplib/url_test.go
+++ b/modules/httplib/url_test.go
@@ -41,19 +41,19 @@ func TestIsRelativeURL(t *testing.T) {
 
 func TestMakeAbsoluteURL(t *testing.T) {
 	defer test.MockVariableValue(&setting.Protocol, "http")()
-	defer test.MockVariableValue(&setting.AppURL, "http://the-host/sub/")()
+	defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
 	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
 
 	ctx := context.Background()
-	assert.Equal(t, "http://the-host/sub/", MakeAbsoluteURL(ctx, ""))
-	assert.Equal(t, "http://the-host/sub/foo", MakeAbsoluteURL(ctx, "foo"))
-	assert.Equal(t, "http://the-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
+	assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, ""))
+	assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "foo"))
+	assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
 	assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo"))
 
 	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
 		Host: "user-host",
 	})
-	assert.Equal(t, "http://user-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
+	assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
 
 	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
 		Host: "user-host",
@@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
 			"X-Forwarded-Host": {"forwarded-host"},
 		},
 	})
-	assert.Equal(t, "https://forwarded-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
+	assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
 
 	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
 		Host: "user-host",
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 1efd166eb3..2a6d44ba08 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -116,6 +116,8 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
 }
 
 func apiUnauthorizedError(ctx *context.Context) {
+	// TODO: it doesn't seem quite right but it doesn't really cause problem at the moment.
+	// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed, ideally.
 	ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`)
 	apiErrorDefined(ctx, errUnauthorized)
 }
diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go
index 782126adf5..6c38f0b509 100644
--- a/routers/web/admin/admin_test.go
+++ b/routers/web/admin/admin_test.go
@@ -87,6 +87,6 @@ func TestSelfCheckPost(t *testing.T) {
 	err := json.Unmarshal(resp.Body.Bytes(), &data)
 	assert.NoError(t, err)
 	assert.Equal(t, []string{
-		ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"),
+		ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://config/sub/"),
 	}, data.Problems)
 }

From 82a0c36332824b8ab41efdf6503e86170ce92f08 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 20 May 2024 00:25:39 +0000
Subject: [PATCH 015/131] [skip ci] Updated licenses and gitignores

---
 options/license/3D-Slicer-1.0                 | 190 ++++++++++++++++++
 .../Asterisk-linking-protocols-exception      |  13 ++
 options/license/HPND-Intel                    |  25 +++
 .../license/HPND-export-US-acknowledgement    |  22 ++
 options/license/NCBI-PD                       |  19 ++
 5 files changed, 269 insertions(+)
 create mode 100644 options/license/3D-Slicer-1.0
 create mode 100644 options/license/Asterisk-linking-protocols-exception
 create mode 100644 options/license/HPND-Intel
 create mode 100644 options/license/HPND-export-US-acknowledgement
 create mode 100644 options/license/NCBI-PD

diff --git a/options/license/3D-Slicer-1.0 b/options/license/3D-Slicer-1.0
new file mode 100644
index 0000000000..38bd5230c6
--- /dev/null
+++ b/options/license/3D-Slicer-1.0
@@ -0,0 +1,190 @@
+3D Slicer Contribution and Software License Agreement ("Agreement")
+Version 1.0 (December 20, 2005)
+
+This Agreement covers contributions to and downloads from the 3D
+Slicer project ("Slicer") maintained by The Brigham and Women's
+Hospital, Inc. ("Brigham"). Part A of this Agreement applies to
+contributions of software and/or data to Slicer (including making
+revisions of or additions to code and/or data already in Slicer). Part
+B of this Agreement applies to downloads of software and/or data from
+Slicer. Part C of this Agreement applies to all transactions with
+Slicer. If you distribute Software (as defined below) downloaded from
+Slicer, all of the paragraphs of Part B of this Agreement must be
+included with and apply to such Software.
+
+Your contribution of software and/or data to Slicer (including prior
+to the date of the first publication of this Agreement, each a
+"Contribution") and/or downloading, copying, modifying, displaying,
+distributing or use of any software and/or data from Slicer
+(collectively, the "Software") constitutes acceptance of all of the
+terms and conditions of this Agreement. If you do not agree to such
+terms and conditions, you have no right to contribute your
+Contribution, or to download, copy, modify, display, distribute or use
+the Software.
+
+PART A. CONTRIBUTION AGREEMENT - License to Brigham with Right to
+Sublicense ("Contribution Agreement").
+
+1. As used in this Contribution Agreement, "you" means the individual
+   contributing the Contribution to Slicer and the institution or
+   entity which employs or is otherwise affiliated with such
+   individual in connection with such Contribution.
+
+2. This Contribution Agreement applies to all Contributions made to
+   Slicer, including without limitation Contributions made prior to
+   the date of first publication of this Agreement. If at any time you
+   make a Contribution to Slicer, you represent that (i) you are
+   legally authorized and entitled to make such Contribution and to
+   grant all licenses granted in this Contribution Agreement with
+   respect to such Contribution; (ii) if your Contribution includes
+   any patient data, all such data is de-identified in accordance with
+   U.S. confidentiality and security laws and requirements, including
+   but not limited to the Health Insurance Portability and
+   Accountability Act (HIPAA) and its regulations, and your disclosure
+   of such data for the purposes contemplated by this Agreement is
+   properly authorized and in compliance with all applicable laws and
+   regulations; and (iii) you have preserved in the Contribution all
+   applicable attributions, copyright notices and licenses for any
+   third party software or data included in the Contribution.
+
+3. Except for the licenses granted in this Agreement, you reserve all
+   right, title and interest in your Contribution.
+
+4. You hereby grant to Brigham, with the right to sublicense, a
+   perpetual, worldwide, non-exclusive, no charge, royalty-free,
+   irrevocable license to use, reproduce, make derivative works of,
+   display and distribute the Contribution. If your Contribution is
+   protected by patent, you hereby grant to Brigham, with the right to
+   sublicense, a perpetual, worldwide, non-exclusive, no-charge,
+   royalty-free, irrevocable license under your interest in patent
+   rights covering the Contribution, to make, have made, use, sell and
+   otherwise transfer your Contribution, alone or in combination with
+   any other code.
+
+5. You acknowledge and agree that Brigham may incorporate your
+   Contribution into Slicer and may make Slicer available to members
+   of the public on an open source basis under terms substantially in
+   accordance with the Software License set forth in Part B of this
+   Agreement. You further acknowledge and agree that Brigham shall
+   have no liability arising in connection with claims resulting from
+   your breach of any of the terms of this Agreement.
+
+6. YOU WARRANT THAT TO THE BEST OF YOUR KNOWLEDGE YOUR CONTRIBUTION
+   DOES NOT CONTAIN ANY CODE THAT REQUIRES OR PRESCRIBES AN "OPEN
+   SOURCE LICENSE" FOR DERIVATIVE WORKS (by way of non-limiting
+   example, the GNU General Public License or other so-called
+   "reciprocal" license that requires any derived work to be licensed
+   under the GNU General Public License or other "open source
+   license").
+
+PART B. DOWNLOADING AGREEMENT - License from Brigham with Right to
+Sublicense ("Software License").
+
+1. As used in this Software License, "you" means the individual
+   downloading and/or using, reproducing, modifying, displaying and/or
+   distributing the Software and the institution or entity which
+   employs or is otherwise affiliated with such individual in
+   connection therewith. The Brigham and Women's Hospital,
+   Inc. ("Brigham") hereby grants you, with right to sublicense, with
+   respect to Brigham's rights in the software, and data, if any,
+   which is the subject of this Software License (collectively, the
+   "Software"), a royalty-free, non-exclusive license to use,
+   reproduce, make derivative works of, display and distribute the
+   Software, provided that:
+
+(a) you accept and adhere to all of the terms and conditions of this
+Software License;
+
+(b) in connection with any copy of or sublicense of all or any portion
+of the Software, all of the terms and conditions in this Software
+License shall appear in and shall apply to such copy and such
+sublicense, including without limitation all source and executable
+forms and on any user documentation, prefaced with the following
+words: "All or portions of this licensed product (such portions are
+the "Software") have been obtained under license from The Brigham and
+Women's Hospital, Inc. and are subject to the following terms and
+conditions:"
+
+(c) you preserve and maintain all applicable attributions, copyright
+notices and licenses included in or applicable to the Software;
+
+(d) modified versions of the Software must be clearly identified and
+marked as such, and must not be misrepresented as being the original
+Software; and
+
+(e) you consider making, but are under no obligation to make, the
+source code of any of your modifications to the Software freely
+available to others on an open source basis.
+
+2. The license granted in this Software License includes without
+   limitation the right to (i) incorporate the Software into
+   proprietary programs (subject to any restrictions applicable to
+   such programs), (ii) add your own copyright statement to your
+   modifications of the Software, and (iii) provide additional or
+   different license terms and conditions in your sublicenses of
+   modifications of the Software; provided that in each case your use,
+   reproduction or distribution of such modifications otherwise
+   complies with the conditions stated in this Software License.
+
+3. This Software License does not grant any rights with respect to
+   third party software, except those rights that Brigham has been
+   authorized by a third party to grant to you, and accordingly you
+   are solely responsible for (i) obtaining any permissions from third
+   parties that you need to use, reproduce, make derivative works of,
+   display and distribute the Software, and (ii) informing your
+   sublicensees, including without limitation your end-users, of their
+   obligations to secure any such required permissions.
+
+4. The Software has been designed for research purposes only and has
+   not been reviewed or approved by the Food and Drug Administration
+   or by any other agency. YOU ACKNOWLEDGE AND AGREE THAT CLINICAL
+   APPLICATIONS ARE NEITHER RECOMMENDED NOR ADVISED. Any
+   commercialization of the Software is at the sole risk of the party
+   or parties engaged in such commercialization. You further agree to
+   use, reproduce, make derivative works of, display and distribute
+   the Software in compliance with all applicable governmental laws,
+   regulations and orders, including without limitation those relating
+   to export and import control.
+
+5. The Software is provided "AS IS" and neither Brigham nor any
+   contributor to the software (each a "Contributor") shall have any
+   obligation to provide maintenance, support, updates, enhancements
+   or modifications thereto. BRIGHAM AND ALL CONTRIBUTORS SPECIFICALLY
+   DISCLAIM ALL EXPRESS AND IMPLIED WARRANTIES OF ANY KIND INCLUDING,
+   BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR
+   A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+   BRIGHAM OR ANY CONTRIBUTOR BE LIABLE TO ANY PARTY FOR DIRECT,
+   INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES
+   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY ARISING IN ANY WAY
+   RELATED TO THE SOFTWARE, EVEN IF BRIGHAM OR ANY CONTRIBUTOR HAS
+   BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. TO THE MAXIMUM
+   EXTENT NOT PROHIBITED BY LAW OR REGULATION, YOU FURTHER ASSUME ALL
+   LIABILITY FOR YOUR USE, REPRODUCTION, MAKING OF DERIVATIVE WORKS,
+   DISPLAY, LICENSE OR DISTRIBUTION OF THE SOFTWARE AND AGREE TO
+   INDEMNIFY AND HOLD HARMLESS BRIGHAM AND ALL CONTRIBUTORS FROM AND
+   AGAINST ANY AND ALL CLAIMS, SUITS, ACTIONS, DEMANDS AND JUDGMENTS
+   ARISING THEREFROM.
+
+6. None of the names, logos or trademarks of Brigham or any of
+   Brigham's affiliates or any of the Contributors, or any funding
+   agency, may be used to endorse or promote products produced in
+   whole or in part by operation of the Software or derived from or
+   based on the Software without specific prior written permission
+   from the applicable party.
+
+7. Any use, reproduction or distribution of the Software which is not
+   in accordance with this Software License shall automatically revoke
+   all rights granted to you under this Software License and render
+   Paragraphs 1 and 2 of this Software License null and void.
+
+8. This Software License does not grant any rights in or to any
+   intellectual property owned by Brigham or any Contributor except
+   those rights expressly granted hereunder.
+
+PART C. MISCELLANEOUS
+
+This Agreement shall be governed by and construed in accordance with
+the laws of The Commonwealth of Massachusetts without regard to
+principles of conflicts of law. This Agreement shall supercede and
+replace any license terms that you may have agreed to previously with
+respect to Slicer.
diff --git a/options/license/Asterisk-linking-protocols-exception b/options/license/Asterisk-linking-protocols-exception
new file mode 100644
index 0000000000..6705829f47
--- /dev/null
+++ b/options/license/Asterisk-linking-protocols-exception
@@ -0,0 +1,13 @@
+Specific permission is also granted to link Asterisk with OpenSSL, OpenH323
+UniMRCP, and/or the UW IMAP Toolkit and distribute the resulting binary files.
+
+In addition, Asterisk implements several management/control protocols.
+This includes the Asterisk Manager Interface (AMI), the Asterisk Gateway
+Interface (AGI), and the Asterisk REST Interface (ARI). It is our belief
+that applications using these protocols to manage or control an Asterisk
+instance do not have to be licensed under the GPL or a compatible license,
+as we believe these protocols do not create a 'derivative work' as referred
+to in the GPL. However, should any court or other judiciary body find that
+these protocols do fall under the terms of the GPL, then we hereby grant you a
+license to use these protocols in combination with Asterisk in external
+applications licensed under any license you wish.
diff --git a/options/license/HPND-Intel b/options/license/HPND-Intel
new file mode 100644
index 0000000000..98f0ceb4fd
--- /dev/null
+++ b/options/license/HPND-Intel
@@ -0,0 +1,25 @@
+Copyright (c) 1993 Intel Corporation
+
+Intel hereby grants you permission to copy, modify, and distribute this
+software and its documentation.  Intel grants this permission provided
+that the above copyright notice appears in all copies and that both the
+copyright notice and this permission notice appear in supporting
+documentation.  In addition, Intel grants this permission provided that
+you prominently mark as "not part of the original" any modifications
+made to this software or documentation, and that the name of Intel
+Corporation not be used in advertising or publicity pertaining to
+distribution of the software or the documentation without specific,
+written prior permission.
+
+Intel Corporation provides this AS IS, WITHOUT ANY WARRANTY, EXPRESS OR
+IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTY OF MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE.  Intel makes no guarantee or
+representations regarding the use of, or the results of the use of,
+the software and documentation in terms of correctness, accuracy,
+reliability, currentness, or otherwise; and you rely on the software,
+documentation and results solely at your own risk.
+
+IN NO EVENT SHALL INTEL BE LIABLE FOR ANY LOSS OF USE, LOSS OF BUSINESS,
+LOSS OF PROFITS, INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES
+OF ANY KIND.  IN NO EVENT SHALL INTEL'S TOTAL LIABILITY EXCEED THE SUM
+PAID TO INTEL FOR THE PRODUCT LICENSED HEREUNDER.
diff --git a/options/license/HPND-export-US-acknowledgement b/options/license/HPND-export-US-acknowledgement
new file mode 100644
index 0000000000..645df4c9aa
--- /dev/null
+++ b/options/license/HPND-export-US-acknowledgement
@@ -0,0 +1,22 @@
+Copyright (C) 1994 by the University of Southern California
+
+   EXPORT OF THIS SOFTWARE from the United States of America may
+   require a specific license from the United States Government. It
+   is the responsibility of any person or organization
+   contemplating export to obtain such a license before exporting.
+
+WITHIN THAT CONSTRAINT, permission to copy, modify, and distribute
+this software and its documentation in source and binary forms is
+hereby granted, provided that any documentation or other materials
+related to such distribution or use acknowledge that the software
+was developed by the University of Southern California.
+
+DISCLAIMER OF WARRANTY.  THIS SOFTWARE IS PROVIDED "AS IS".  The
+University of Southern California MAKES NO REPRESENTATIONS OR
+WARRANTIES, EXPRESS OR IMPLIED.  By way of example, but not
+limitation, the University of Southern California MAKES NO
+REPRESENTATIONS OR WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY
+PARTICULAR PURPOSE. The University of Southern California shall not
+be held liable for any liability nor for any direct, indirect, or
+consequential damages with respect to any claim by the user or
+distributor of the ksu software.
diff --git a/options/license/NCBI-PD b/options/license/NCBI-PD
new file mode 100644
index 0000000000..d838cf36b9
--- /dev/null
+++ b/options/license/NCBI-PD
@@ -0,0 +1,19 @@
+PUBLIC DOMAIN NOTICE
+National Center for Biotechnology Information
+
+This software is a "United States Government Work" under the terms of the
+United States Copyright Act.  It was written as part of the authors'
+official duties as United States Government employees and thus cannot
+be copyrighted.  This software is freely available to the public for
+use. The National Library of Medicine and the U.S. Government have not
+placed any restriction on its use or reproduction.
+
+Although all reasonable efforts have been taken to ensure the accuracy
+and reliability of the software and data, the NLM and the U.S.
+Government do not and cannot warrant the performance or results that
+may be obtained by using this software or data. The NLM and the U.S.
+Government disclaim all warranties, express or implied, including
+warranties of performance, merchantability or fitness for any
+particular purpose.
+
+Please cite the author in any work or product based on this material.

From edbf74c418061b013a5855f604dd6be6baf34132 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 20 May 2024 08:56:45 +0800
Subject: [PATCH 016/131] Fix "force private" logic (#31012)

When creating a repo, the "FORCE_PRIVATE" config option should be
respected, `readonly` doesn't work for checkbox, so it should use
`disabled` attribute.
---
 routers/api/v1/repo/migrate.go        | 2 +-
 routers/api/v1/repo/repo.go           | 4 ++--
 routers/web/repo/repo.go              | 2 +-
 services/migrations/gitea_uploader.go | 2 +-
 services/repository/repository.go     | 2 +-
 services/task/task.go                 | 2 +-
 templates/repo/create.tmpl            | 2 +-
 templates/repo/migrate/codebase.tmpl  | 2 +-
 templates/repo/migrate/git.tmpl       | 2 +-
 templates/repo/migrate/gitbucket.tmpl | 2 +-
 templates/repo/migrate/gitea.tmpl     | 2 +-
 templates/repo/migrate/github.tmpl    | 2 +-
 templates/repo/migrate/gitlab.tmpl    | 2 +-
 templates/repo/migrate/gogs.tmpl      | 2 +-
 templates/repo/migrate/onedev.tmpl    | 2 +-
 templates/repo/settings/options.tmpl  | 5 +++--
 16 files changed, 19 insertions(+), 18 deletions(-)

diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go
index f246b08c0a..14c8c01f4e 100644
--- a/routers/api/v1/repo/migrate.go
+++ b/routers/api/v1/repo/migrate.go
@@ -175,7 +175,7 @@ func Migrate(ctx *context.APIContext) {
 		Description:    opts.Description,
 		OriginalURL:    form.CloneAddr,
 		GitServiceType: gitServiceType,
-		IsPrivate:      opts.Private,
+		IsPrivate:      opts.Private || setting.Repository.ForcePrivate,
 		IsMirror:       opts.Mirror,
 		Status:         repo_model.RepositoryBeingMigrated,
 	})
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index e759142938..594f2d86f6 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -252,7 +252,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
 		Gitignores:       opt.Gitignores,
 		License:          opt.License,
 		Readme:           opt.Readme,
-		IsPrivate:        opt.Private,
+		IsPrivate:        opt.Private || setting.Repository.ForcePrivate,
 		AutoInit:         opt.AutoInit,
 		DefaultBranch:    opt.DefaultBranch,
 		TrustModel:       repo_model.ToTrustModel(opt.TrustModel),
@@ -364,7 +364,7 @@ func Generate(ctx *context.APIContext) {
 		Name:            form.Name,
 		DefaultBranch:   form.DefaultBranch,
 		Description:     form.Description,
-		Private:         form.Private,
+		Private:         form.Private || setting.Repository.ForcePrivate,
 		GitContent:      form.GitContent,
 		Topics:          form.Topics,
 		GitHooks:        form.GitHooks,
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 48be1c2296..71c582b5f9 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -248,7 +248,7 @@ func CreatePost(ctx *context.Context) {
 		opts := repo_service.GenerateRepoOptions{
 			Name:            form.RepoName,
 			Description:     form.Description,
-			Private:         form.Private,
+			Private:         form.Private || setting.Repository.ForcePrivate,
 			GitContent:      form.GitContent,
 			Topics:          form.Topics,
 			GitHooks:        form.GitHooks,
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index c63383f5ca..4c8e036f05 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -107,7 +107,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
 			Description:    repo.Description,
 			OriginalURL:    repo.OriginalURL,
 			GitServiceType: opts.GitServiceType,
-			IsPrivate:      opts.Private,
+			IsPrivate:      opts.Private || setting.Repository.ForcePrivate,
 			IsMirror:       opts.Mirror,
 			Status:         repo_model.RepositoryBeingMigrated,
 		})
diff --git a/services/repository/repository.go b/services/repository/repository.go
index d28200c0ad..b7aac3cfe0 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -85,7 +85,7 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN
 
 	repo, err := CreateRepository(ctx, authUser, owner, CreateRepoOptions{
 		Name:      repoName,
-		IsPrivate: setting.Repository.DefaultPushCreatePrivate,
+		IsPrivate: setting.Repository.DefaultPushCreatePrivate || setting.Repository.ForcePrivate,
 	})
 	if err != nil {
 		return nil, err
diff --git a/services/task/task.go b/services/task/task.go
index e15cab7b3c..c90ee91270 100644
--- a/services/task/task.go
+++ b/services/task/task.go
@@ -107,7 +107,7 @@ func CreateMigrateTask(ctx context.Context, doer, u *user_model.User, opts base.
 		Description:    opts.Description,
 		OriginalURL:    opts.OriginalURL,
 		GitServiceType: opts.GitServiceType,
-		IsPrivate:      opts.Private,
+		IsPrivate:      opts.Private || setting.Repository.ForcePrivate,
 		IsMirror:       opts.Mirror,
 		Status:         repo_model.RepositoryBeingMigrated,
 	})
diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl
index c1c8c2185e..2e1de244ea 100644
--- a/templates/repo/create.tmpl
+++ b/templates/repo/create.tmpl
@@ -50,7 +50,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
diff --git a/templates/repo/migrate/codebase.tmpl b/templates/repo/migrate/codebase.tmpl
index 439a883863..c8059b7c7b 100644
--- a/templates/repo/migrate/codebase.tmpl
+++ b/templates/repo/migrate/codebase.tmpl
@@ -89,7 +89,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl
index db01b8d858..9c5f0d7d6d 100644
--- a/templates/repo/migrate/git.tmpl
+++ b/templates/repo/migrate/git.tmpl
@@ -63,7 +63,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
diff --git a/templates/repo/migrate/gitbucket.tmpl b/templates/repo/migrate/gitbucket.tmpl
index d1f1db99ba..b667fa828a 100644
--- a/templates/repo/migrate/gitbucket.tmpl
+++ b/templates/repo/migrate/gitbucket.tmpl
@@ -105,7 +105,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
diff --git a/templates/repo/migrate/gitea.tmpl b/templates/repo/migrate/gitea.tmpl
index 143f220449..3b8f377096 100644
--- a/templates/repo/migrate/gitea.tmpl
+++ b/templates/repo/migrate/gitea.tmpl
@@ -101,7 +101,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}} checked{{end}}>
diff --git a/templates/repo/migrate/github.tmpl b/templates/repo/migrate/github.tmpl
index dfb2b4bc46..3535eddfc2 100644
--- a/templates/repo/migrate/github.tmpl
+++ b/templates/repo/migrate/github.tmpl
@@ -103,7 +103,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl
index 76c2828257..f705fb3090 100644
--- a/templates/repo/migrate/gitlab.tmpl
+++ b/templates/repo/migrate/gitlab.tmpl
@@ -100,7 +100,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
diff --git a/templates/repo/migrate/gogs.tmpl b/templates/repo/migrate/gogs.tmpl
index b01d0eeb67..eca83b1636 100644
--- a/templates/repo/migrate/gogs.tmpl
+++ b/templates/repo/migrate/gogs.tmpl
@@ -103,7 +103,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}} checked{{end}}>
diff --git a/templates/repo/migrate/onedev.tmpl b/templates/repo/migrate/onedev.tmpl
index 8b2a2d8730..e1aad96ba4 100644
--- a/templates/repo/migrate/onedev.tmpl
+++ b/templates/repo/migrate/onedev.tmpl
@@ -89,7 +89,7 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox">
 							{{if .IsForcedPrivate}}
-								<input name="private" type="checkbox" checked readonly>
+								<input name="private" type="checkbox" checked disabled>
 								<label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label>
 							{{else}}
 								<input name="private" type="checkbox" {{if .private}}checked{{end}}>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index b94c202f16..3168384072 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -28,9 +28,10 @@
 						<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
 						<div class="ui checkbox" {{if and (not .Repository.IsPrivate) (gt .Repository.NumStars 0)}}data-tooltip-content="{{ctx.Locale.Tr "repo.stars_remove_warning"}}"{{end}}>
 							{{if .IsAdmin}}
-							<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}>
+								<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}>
 							{{else}}
-							<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}{{if and $.ForcePrivate .Repository.IsPrivate}} readonly{{end}}>
+								<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}{{if and $.ForcePrivate .Repository.IsPrivate}} disabled{{end}}>
+								{{if and .Repository.IsPrivate $.ForcePrivate}}<input type="hidden" name="private" value="{{.Repository.IsPrivate}}">{{end}}
 							{{end}}
 							<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>

From 47accfebbd69e5f47d1b97a3e39cf181fab7e597 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 20 May 2024 12:35:38 +0800
Subject: [PATCH 017/131] Fix data-race during testing (#30999)

Fix #30992
---
 models/unit/unit.go                   | 26 ++++++++++++++++++++------
 models/unit/unit_test.go              | 24 ++++++++++++------------
 tests/integration/org_project_test.go |  6 ++++--
 3 files changed, 36 insertions(+), 20 deletions(-)

diff --git a/models/unit/unit.go b/models/unit/unit.go
index a78a2f1e47..74efa4caf0 100644
--- a/models/unit/unit.go
+++ b/models/unit/unit.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"strings"
+	"sync/atomic"
 
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/modules/container"
@@ -106,10 +107,23 @@ var (
 		TypeExternalTracker,
 	}
 
-	// DisabledRepoUnits contains the units that have been globally disabled
-	DisabledRepoUnits = []Type{}
+	disabledRepoUnitsAtomic atomic.Pointer[[]Type] // the units that have been globally disabled
 )
 
+// DisabledRepoUnitsGet returns the globally disabled units, it is a quick patch to fix data-race during testing.
+// Because the queue worker might read when a test is mocking the value. FIXME: refactor to a clear solution later.
+func DisabledRepoUnitsGet() []Type {
+	v := disabledRepoUnitsAtomic.Load()
+	if v == nil {
+		return nil
+	}
+	return *v
+}
+
+func DisabledRepoUnitsSet(v []Type) {
+	disabledRepoUnitsAtomic.Store(&v)
+}
+
 // Get valid set of default repository units from settings
 func validateDefaultRepoUnits(defaultUnits, settingDefaultUnits []Type) []Type {
 	units := defaultUnits
@@ -127,7 +141,7 @@ func validateDefaultRepoUnits(defaultUnits, settingDefaultUnits []Type) []Type {
 	}
 
 	// Remove disabled units
-	for _, disabledUnit := range DisabledRepoUnits {
+	for _, disabledUnit := range DisabledRepoUnitsGet() {
 		for i, unit := range units {
 			if unit == disabledUnit {
 				units = append(units[:i], units[i+1:]...)
@@ -140,11 +154,11 @@ func validateDefaultRepoUnits(defaultUnits, settingDefaultUnits []Type) []Type {
 
 // LoadUnitConfig load units from settings
 func LoadUnitConfig() error {
-	var invalidKeys []string
-	DisabledRepoUnits, invalidKeys = FindUnitTypes(setting.Repository.DisabledRepoUnits...)
+	disabledRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DisabledRepoUnits...)
 	if len(invalidKeys) > 0 {
 		log.Warn("Invalid keys in disabled repo units: %s", strings.Join(invalidKeys, ", "))
 	}
+	DisabledRepoUnitsSet(disabledRepoUnits)
 
 	setDefaultRepoUnits, invalidKeys := FindUnitTypes(setting.Repository.DefaultRepoUnits...)
 	if len(invalidKeys) > 0 {
@@ -167,7 +181,7 @@ func LoadUnitConfig() error {
 
 // UnitGlobalDisabled checks if unit type is global disabled
 func (u Type) UnitGlobalDisabled() bool {
-	for _, ud := range DisabledRepoUnits {
+	for _, ud := range DisabledRepoUnitsGet() {
 		if u == ud {
 			return true
 		}
diff --git a/models/unit/unit_test.go b/models/unit/unit_test.go
index d80d8b118d..7bf6326145 100644
--- a/models/unit/unit_test.go
+++ b/models/unit/unit_test.go
@@ -14,10 +14,10 @@ import (
 func TestLoadUnitConfig(t *testing.T) {
 	t.Run("regular", func(t *testing.T) {
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) {
-			DisabledRepoUnits = disabledRepoUnits
+			DisabledRepoUnitsSet(disabledRepoUnits)
 			DefaultRepoUnits = defaultRepoUnits
 			DefaultForkRepoUnits = defaultForkRepoUnits
-		}(DisabledRepoUnits, DefaultRepoUnits, DefaultForkRepoUnits)
+		}(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits)
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) {
 			setting.Repository.DisabledRepoUnits = disabledRepoUnits
 			setting.Repository.DefaultRepoUnits = defaultRepoUnits
@@ -28,16 +28,16 @@ func TestLoadUnitConfig(t *testing.T) {
 		setting.Repository.DefaultRepoUnits = []string{"repo.code", "repo.releases", "repo.issues", "repo.pulls"}
 		setting.Repository.DefaultForkRepoUnits = []string{"repo.releases"}
 		assert.NoError(t, LoadUnitConfig())
-		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnits)
+		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet())
 		assert.Equal(t, []Type{TypeCode, TypeReleases, TypePullRequests}, DefaultRepoUnits)
 		assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits)
 	})
 	t.Run("invalid", func(t *testing.T) {
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) {
-			DisabledRepoUnits = disabledRepoUnits
+			DisabledRepoUnitsSet(disabledRepoUnits)
 			DefaultRepoUnits = defaultRepoUnits
 			DefaultForkRepoUnits = defaultForkRepoUnits
-		}(DisabledRepoUnits, DefaultRepoUnits, DefaultForkRepoUnits)
+		}(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits)
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) {
 			setting.Repository.DisabledRepoUnits = disabledRepoUnits
 			setting.Repository.DefaultRepoUnits = defaultRepoUnits
@@ -48,16 +48,16 @@ func TestLoadUnitConfig(t *testing.T) {
 		setting.Repository.DefaultRepoUnits = []string{"repo.code", "invalid.2", "repo.releases", "repo.issues", "repo.pulls"}
 		setting.Repository.DefaultForkRepoUnits = []string{"invalid.3", "repo.releases"}
 		assert.NoError(t, LoadUnitConfig())
-		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnits)
+		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet())
 		assert.Equal(t, []Type{TypeCode, TypeReleases, TypePullRequests}, DefaultRepoUnits)
 		assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits)
 	})
 	t.Run("duplicate", func(t *testing.T) {
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) {
-			DisabledRepoUnits = disabledRepoUnits
+			DisabledRepoUnitsSet(disabledRepoUnits)
 			DefaultRepoUnits = defaultRepoUnits
 			DefaultForkRepoUnits = defaultForkRepoUnits
-		}(DisabledRepoUnits, DefaultRepoUnits, DefaultForkRepoUnits)
+		}(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits)
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) {
 			setting.Repository.DisabledRepoUnits = disabledRepoUnits
 			setting.Repository.DefaultRepoUnits = defaultRepoUnits
@@ -68,16 +68,16 @@ func TestLoadUnitConfig(t *testing.T) {
 		setting.Repository.DefaultRepoUnits = []string{"repo.code", "repo.releases", "repo.issues", "repo.pulls", "repo.code"}
 		setting.Repository.DefaultForkRepoUnits = []string{"repo.releases", "repo.releases"}
 		assert.NoError(t, LoadUnitConfig())
-		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnits)
+		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet())
 		assert.Equal(t, []Type{TypeCode, TypeReleases, TypePullRequests}, DefaultRepoUnits)
 		assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits)
 	})
 	t.Run("empty_default", func(t *testing.T) {
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) {
-			DisabledRepoUnits = disabledRepoUnits
+			DisabledRepoUnitsSet(disabledRepoUnits)
 			DefaultRepoUnits = defaultRepoUnits
 			DefaultForkRepoUnits = defaultForkRepoUnits
-		}(DisabledRepoUnits, DefaultRepoUnits, DefaultForkRepoUnits)
+		}(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits)
 		defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) {
 			setting.Repository.DisabledRepoUnits = disabledRepoUnits
 			setting.Repository.DefaultRepoUnits = defaultRepoUnits
@@ -88,7 +88,7 @@ func TestLoadUnitConfig(t *testing.T) {
 		setting.Repository.DefaultRepoUnits = []string{}
 		setting.Repository.DefaultForkRepoUnits = []string{"repo.releases", "repo.releases"}
 		assert.NoError(t, LoadUnitConfig())
-		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnits)
+		assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet())
 		assert.ElementsMatch(t, []Type{TypeCode, TypePullRequests, TypeReleases, TypeWiki, TypePackages, TypeProjects, TypeActions}, DefaultRepoUnits)
 		assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits)
 	})
diff --git a/tests/integration/org_project_test.go b/tests/integration/org_project_test.go
index ca39cf5130..31d10f16ff 100644
--- a/tests/integration/org_project_test.go
+++ b/tests/integration/org_project_test.go
@@ -9,13 +9,15 @@ import (
 	"testing"
 
 	unit_model "code.gitea.io/gitea/models/unit"
-	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
 )
 
 func TestOrgProjectAccess(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
-	defer test.MockVariableValue(&unit_model.DisabledRepoUnits, append(slices.Clone(unit_model.DisabledRepoUnits), unit_model.TypeProjects))()
+
+	disabledRepoUnits := unit_model.DisabledRepoUnitsGet()
+	unit_model.DisabledRepoUnitsSet(append(slices.Clone(disabledRepoUnits), unit_model.TypeProjects))
+	defer unit_model.DisabledRepoUnitsSet(disabledRepoUnits)
 
 	// repo project, 404
 	req := NewRequest(t, "GET", "/user2/repo1/projects")

From b6574099edbb47e119762700f637c8da349cca2b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 20 May 2024 13:21:01 +0800
Subject: [PATCH 018/131] Fix project column title overflow (#31011)

By the way:
* Re-format the "color.go" to Golang code style
* Remove unused `overflow-y: scroll;` from `.project-column` because
there is `overflow: visible`
---
 modules/util/color.go             |  9 +++++----
 templates/projects/view.tmpl      | 16 ++++++----------
 web_src/css/features/projects.css | 13 +++++--------
 3 files changed, 16 insertions(+), 22 deletions(-)

diff --git a/modules/util/color.go b/modules/util/color.go
index 9c520dce78..8fffc91ac4 100644
--- a/modules/util/color.go
+++ b/modules/util/color.go
@@ -1,5 +1,6 @@
 // Copyright 2023 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
+
 package util
 
 import (
@@ -8,7 +9,7 @@ import (
 	"strings"
 )
 
-// Get color as RGB values in 0..255 range from the hex color string (with or without #)
+// HexToRBGColor parses 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
 	if strings.HasPrefix(colorString, "#") {
@@ -35,7 +36,7 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
 	return r, g, b
 }
 
-// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
+// GetRelativeLuminance 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)
@@ -46,8 +47,8 @@ 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.
+// ContrastColor 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) {
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 47f214a44e..45c8461218 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -68,18 +68,14 @@
 		{{range .Columns}}
 			<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>
-						<span class="project-column-title-label">{{.Title}}</span>
+					<div class="ui circular label project-column-issue-count">
+						{{.NumIssues ctx}}
 					</div>
+					<div class="project-column-title-label gt-ellipsis">{{.Title}}</div>
 					{{if $canWriteProject}}
-						<div class="ui dropdown jump item">
-							<div class="tw-px-2">
-								{{svg "octicon-kebab-horizontal"}}
-							</div>
-							<div class="menu user-menu">
+						<div class="ui dropdown tw-p-1">
+							{{svg "octicon-kebab-horizontal"}}
+							<div class="menu">
 								<a class="item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}">
 									{{svg "octicon-pencil"}}
 									{{ctx.Locale.Tr "repo.projects.column.edit"}}
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index e23c146748..21e2aee0a2 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -14,7 +14,6 @@
   width: 320px;
   height: calc(100vh - 450px);
   min-height: 60vh;
-  overflow-y: scroll;
   flex: 0 0 auto;
   overflow: visible;
   display: flex;
@@ -30,17 +29,15 @@
   display: flex;
   align-items: center;
   justify-content: space-between;
+  gap: 0.5em;
 }
 
-.project-column-title {
-  background: none !important;
-  line-height: 1.25 !important;
-  cursor: inherit;
+.ui.label.project-column-issue-count {
+  color: inherit;
 }
 
-.project-column-title,
-.project-column-issue-count {
-  color: inherit !important;
+.project-column-title-label {
+  flex: 1;
 }
 
 .project-column > .cards {

From f48cc501c46a2d34eb701561f01d888d689d60d5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 20 May 2024 13:57:57 +0800
Subject: [PATCH 019/131] Fix incorrect "blob excerpt" link when comparing
 files (#31013)

When comparing files between the base repo and forked repo, the "blob
excerpt" link should point to the forked repo, because the commit
doesn't exist in base repo.

Co-authored-by: Giteabot <teabot@gitea.io>
---
 templates/repo/diff/section_split.tmpl   |  7 +++--
 templates/repo/diff/section_unified.tmpl |  7 +++--
 tests/integration/compare_test.go        | 39 ++++++++++++++++++++++++
 3 files changed, 47 insertions(+), 6 deletions(-)

diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index 67e2b195de..349f0c3dfc 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -1,4 +1,5 @@
 {{$file := .file}}
+{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}}
 <colgroup>
 	<col width="50">
 	<col width="10">
@@ -18,17 +19,17 @@
 					<td class="lines-num lines-num-old">
 						<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}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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" 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}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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" 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}}">
+							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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 4111159709..ec59f4d42e 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -1,4 +1,5 @@
 {{$file := .file}}
+{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}}
 <colgroup>
 	<col width="50">
 	<col width="50">
@@ -14,17 +15,17 @@
 					<td colspan="2" class="lines-num">
 						<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}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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" 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}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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" 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}}">
+								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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/tests/integration/compare_test.go b/tests/integration/compare_test.go
index 27b2920cc1..7fb8dbc332 100644
--- a/tests/integration/compare_test.go
+++ b/tests/integration/compare_test.go
@@ -6,9 +6,14 @@ package integration
 import (
 	"fmt"
 	"net/http"
+	"net/url"
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	repo_service "code.gitea.io/gitea/services/repository"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -118,3 +123,37 @@ func TestCompareBranches(t *testing.T) {
 
 	inspectCompare(t, htmlDoc, diffCount, diffChanges)
 }
+
+func TestCompareCodeExpand(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+		repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user1, user1, repo_service.CreateRepoOptions{
+			Name:          "test_blob_excerpt",
+			Readme:        "Default",
+			AutoInit:      true,
+			DefaultBranch: "main",
+		})
+		assert.NoError(t, err)
+
+		session := loginUser(t, user1.Name)
+		testEditFile(t, session, user1.Name, repo.Name, "main", "README.md", strings.Repeat("a\n", 30))
+
+		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+		session = loginUser(t, user2.Name)
+		testRepoFork(t, session, user1.Name, repo.Name, user2.Name, "test_blob_excerpt-fork")
+		testCreateBranch(t, session, user2.Name, "test_blob_excerpt-fork", "branch/main", "forked-branch", http.StatusSeeOther)
+		testEditFile(t, session, user2.Name, "test_blob_excerpt-fork", "forked-branch", "README.md", strings.Repeat("a\n", 15)+"CHANGED\n"+strings.Repeat("a\n", 15))
+
+		req := NewRequest(t, "GET", "/user1/test_blob_excerpt/compare/main...user2/test_blob_excerpt-fork:forked-branch")
+		resp := session.MakeRequest(t, req, http.StatusOK)
+		htmlDoc := NewHTMLParser(t, resp.Body)
+		els := htmlDoc.Find(`button.code-expander-button[hx-get]`)
+
+		// all the links in the comparison should be to the forked repo&branch
+		assert.NotZero(t, els.Length())
+		for i := 0; i < els.Length(); i++ {
+			link := els.Eq(i).AttrOr("hx-get", "")
+			assert.True(t, strings.HasPrefix(link, "/user2/test_blob_excerpt-fork/blob_excerpt/"))
+		}
+	})
+}

From de9bcd1d23523d736234ccbf73adce4746575e1b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 20 May 2024 14:44:16 +0800
Subject: [PATCH 020/131] Avoid 500 panic error when uploading invalid maven
 package file (#31014)

PackageDescriptor.Metadata might be nil (and maybe not only for maven).
This is only a quick fix.

The new `if` block is written intentionally to avoid unnecessary
indenting to the existing code.
---
 options/locale/locale_en-US.ini              |  1 +
 templates/package/content/maven.tmpl         |  6 +++++-
 templates/package/metadata/maven.tmpl        |  5 ++++-
 tests/integration/api_packages_maven_test.go | 10 ++++++++++
 4 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a85b107eee..db4e3ec56b 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3415,6 +3415,7 @@ error.unit_not_allowed = You are not allowed to access this repository section.
 title = Packages
 desc = Manage repository packages.
 empty = There are no packages yet.
+no_metadata = No metadata.
 empty.documentation = For more information on the package registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
 empty.repo = Did you upload a package, but it's not shown here? Go to <a href="%[1]s">package settings</a> and link it to this repo.
 registry.documentation = For more information on the %s registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl
index 3a7de335de..f56595a830 100644
--- a/templates/package/content/maven.tmpl
+++ b/templates/package/content/maven.tmpl
@@ -1,4 +1,8 @@
-{{if eq .PackageDescriptor.Package.Type "maven"}}
+{{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}}
+	<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.installation"}}</h4>
+	<div class="ui attached segment">{{ctx.Locale.Tr "packages.no_metadata"}}</div>
+{{end}}
+{{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}}
 	<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.installation"}}</h4>
 	<div class="ui attached segment">
 		<div class="ui form">
diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl
index 548be61790..36412723d2 100644
--- a/templates/package/metadata/maven.tmpl
+++ b/templates/package/metadata/maven.tmpl
@@ -1,4 +1,7 @@
-{{if eq .PackageDescriptor.Package.Type "maven"}}
+{{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}}
+	<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.no_metadata"}}</div>
+{{end}}
+{{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}}
 	{{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}}
diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go
index c7ed554a9d..0466a727b2 100644
--- a/tests/integration/api_packages_maven_test.go
+++ b/tests/integration/api_packages_maven_test.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/packages/maven"
+	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -241,4 +242,13 @@ func TestPackageMaven(t *testing.T) {
 		putFile(t, fmt.Sprintf("/%s/maven-metadata.xml", snapshotVersion), "test", http.StatusCreated)
 		putFile(t, fmt.Sprintf("/%s/maven-metadata.xml", snapshotVersion), "test-overwrite", http.StatusCreated)
 	})
+
+	t.Run("InvalidFile", func(t *testing.T) {
+		ver := packageVersion + "-invalid"
+		putFile(t, fmt.Sprintf("/%s/%s", ver, filename), "any invalid content", http.StatusCreated)
+		req := NewRequestf(t, "GET", "/%s/-/packages/maven/%s-%s/%s", user.Name, groupID, artifactID, ver)
+		resp := MakeRequest(t, req, http.StatusOK)
+		assert.Contains(t, resp.Body.String(), "No metadata.")
+		assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
+	})
 }

From f1d9f18d96050d89a4085c961f572f07b1e653d1 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Mon, 20 May 2024 15:17:00 +0800
Subject: [PATCH 021/131] Return `access_denied` error when an OAuth2 request
 is denied (#30974)

According to [RFC
6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1),
when the resource owner or authorization server denied an request, an
`access_denied` error should be returned. But currently in this case
Gitea does not return any error.

For example, if the user clicks "Cancel" here, an `access_denied` error
should be returned.

<img width="360px"
src="https://github.com/go-gitea/gitea/assets/15528715/be31c09b-4c0a-4701-b7a4-f54b8fe3a6c5"
/>
---
 routers/web/auth/oauth.go      | 10 ++++++++++
 services/forms/user_form.go    |  1 +
 templates/user/auth/grant.tmpl |  4 ++--
 3 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 354e70bcbf..84fa473044 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -541,6 +541,16 @@ func GrantApplicationOAuth(ctx *context.Context) {
 		ctx.Error(http.StatusBadRequest)
 		return
 	}
+
+	if !form.Granted {
+		handleAuthorizeError(ctx, AuthorizeError{
+			State:            form.State,
+			ErrorDescription: "the request is denied",
+			ErrorCode:        ErrorCodeAccessDenied,
+		}, form.RedirectURI)
+		return
+	}
+
 	app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
 	if err != nil {
 		ctx.ServerError("GetOAuth2ApplicationByClientID", err)
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 418a87b863..b4be1e02b7 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -161,6 +161,7 @@ func (f *AuthorizationForm) Validate(req *http.Request, errs binding.Errors) bin
 // GrantApplicationForm form for authorizing oauth2 clients
 type GrantApplicationForm struct {
 	ClientID    string `binding:"Required"`
+	Granted     bool
 	RedirectURI string
 	State       string
 	Scope       string
diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl
index cb9bba8749..a18a3bd27a 100644
--- a/templates/user/auth/grant.tmpl
+++ b/templates/user/auth/grant.tmpl
@@ -23,8 +23,8 @@
 					<input type="hidden" name="scope" value="{{.Scope}}">
 					<input type="hidden" name="nonce" value="{{.Nonce}}">
 					<input type="hidden" name="redirect_uri" value="{{.RedirectURI}}">
-					<button type="submit" id="authorize-app" value="{{ctx.Locale.Tr "auth.authorize_application"}}" class="ui red inline button">{{ctx.Locale.Tr "auth.authorize_application"}}</button>
-					<a href="{{.RedirectURI}}" class="ui basic primary inline button">Cancel</a>
+					<button type="submit" id="authorize-app" name="granted" value="true" class="ui red inline button">{{ctx.Locale.Tr "auth.authorize_application"}}</button>
+					<button type="submit" name="granted" value="false" class="ui basic primary inline button">{{ctx.Locale.Tr "cancel"}}</button>
 				</form>
 			</div>
 		</div>

From fb1ad920b769799aa1287441289d15477d9878c5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 20 May 2024 23:12:50 +0800
Subject: [PATCH 022/131] Refactor sha1 and time-limited code (#31023)

Remove "EncodeSha1", it shouldn't be used as a general purpose hasher
(just like we have removed "EncodeMD5" in #28622)

Rewrite the "time-limited code" related code and write better tests, the
old code doesn't seem quite right.
---
 models/user/email_address.go |  5 +-
 models/user/user.go          |  7 +--
 modules/base/tool.go         | 85 ++++++++++++++++------------------
 modules/base/tool_test.go    | 89 ++++++++++++++++++++----------------
 modules/git/utils.go         |  8 ++++
 modules/git/utils_test.go    | 17 +++++++
 routers/web/repo/compare.go  |  2 +-
 services/gitdiff/gitdiff.go  |  3 +-
 8 files changed, 120 insertions(+), 96 deletions(-)
 create mode 100644 modules/git/utils_test.go

diff --git a/models/user/email_address.go b/models/user/email_address.go
index 08771efe99..71b96c00be 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -10,6 +10,7 @@ import (
 	"net/mail"
 	"regexp"
 	"strings"
+	"time"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
@@ -353,14 +354,12 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne
 
 // VerifyActiveEmailCode verifies active email code when active account
 func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
-	minutes := setting.Service.ActiveCodeLives
-
 	if user := GetVerifyUser(ctx, code); user != nil {
 		// time limit code
 		prefix := code[:base.TimeLimitCodeLength]
 		data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
 
-		if base.VerifyTimeLimitCode(data, minutes, prefix) {
+		if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
 			emailAddress := &EmailAddress{UID: user.ID, Email: email}
 			if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
 				return emailAddress
diff --git a/models/user/user.go b/models/user/user.go
index a5a5b5bdf6..6848d1be95 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -304,7 +304,7 @@ func (u *User) OrganisationLink() string {
 func (u *User) GenerateEmailActivateCode(email string) string {
 	code := base.CreateTimeLimitCode(
 		fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
-		setting.Service.ActiveCodeLives, nil)
+		setting.Service.ActiveCodeLives, time.Now(), nil)
 
 	// Add tail hex username
 	code += hex.EncodeToString([]byte(u.LowerName))
@@ -791,14 +791,11 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) {
 
 // VerifyUserActiveCode verifies active code when active account
 func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
-	minutes := setting.Service.ActiveCodeLives
-
 	if user = GetVerifyUser(ctx, code); user != nil {
 		// time limit code
 		prefix := code[:base.TimeLimitCodeLength]
 		data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
-
-		if base.VerifyTimeLimitCode(data, minutes, prefix) {
+		if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
 			return user
 		}
 	}
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 40785e74e8..378eb7e3dd 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -4,12 +4,15 @@
 package base
 
 import (
+	"crypto/hmac"
 	"crypto/sha1"
 	"crypto/sha256"
+	"crypto/subtle"
 	"encoding/base64"
 	"encoding/hex"
 	"errors"
 	"fmt"
+	"hash"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -25,13 +28,6 @@ import (
 	"github.com/dustin/go-humanize"
 )
 
-// EncodeSha1 string to sha1 hex value.
-func EncodeSha1(str string) string {
-	h := sha1.New()
-	_, _ = h.Write([]byte(str))
-	return hex.EncodeToString(h.Sum(nil))
-}
-
 // EncodeSha256 string to sha256 hex value.
 func EncodeSha256(str string) string {
 	h := sha256.New()
@@ -62,63 +58,62 @@ func BasicAuthDecode(encoded string) (string, string, error) {
 }
 
 // VerifyTimeLimitCode verify time limit code
-func VerifyTimeLimitCode(data string, minutes int, code string) bool {
+func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool {
 	if len(code) <= 18 {
 		return false
 	}
 
-	// split code
-	start := code[:12]
-	lives := code[12:18]
-	if d, err := strconv.ParseInt(lives, 10, 0); err == nil {
-		minutes = int(d)
-	}
+	startTimeStr := code[:12]
+	aliveTimeStr := code[12:18]
+	aliveTime, _ := strconv.Atoi(aliveTimeStr) // no need to check err, if anything wrong, the following code check will fail soon
 
-	// right active code
-	retCode := CreateTimeLimitCode(data, minutes, start)
-	if retCode == code && minutes > 0 {
-		// check time is expired or not
-		before, _ := time.ParseInLocation("200601021504", start, time.Local)
-		now := time.Now()
-		if before.Add(time.Minute*time.Duration(minutes)).Unix() > now.Unix() {
-			return true
+	// check code
+	retCode := CreateTimeLimitCode(data, aliveTime, startTimeStr, nil)
+	if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
+		retCode = CreateTimeLimitCode(data, aliveTime, startTimeStr, sha1.New()) // TODO: this is only for the support of legacy codes, remove this in/after 1.23
+		if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
+			return false
 		}
 	}
 
-	return false
+	// check time is expired or not: startTime <= now && now < startTime + minutes
+	startTime, _ := time.ParseInLocation("200601021504", startTimeStr, time.Local)
+	return (startTime.Before(now) || startTime.Equal(now)) && now.Before(startTime.Add(time.Minute*time.Duration(minutes)))
 }
 
 // TimeLimitCodeLength default value for time limit code
 const TimeLimitCodeLength = 12 + 6 + 40
 
-// CreateTimeLimitCode create a time limit code
-// code format: 12 length date time string + 6 minutes string + 40 sha1 encoded string
-func CreateTimeLimitCode(data string, minutes int, startInf any) string {
-	format := "200601021504"
+// CreateTimeLimitCode create a time-limited code.
+// Format: 12 length date time string + 6 minutes string (not used) + 40 hash string, some other code depends on this fixed length
+// If h is nil, then use the default hmac hash.
+func CreateTimeLimitCode[T time.Time | string](data string, minutes int, startTimeGeneric T, h hash.Hash) string {
+	const format = "200601021504"
 
-	var start, end time.Time
-	var startStr, endStr string
-
-	if startInf == nil {
-		// Use now time create code
-		start = time.Now()
-		startStr = start.Format(format)
+	var start time.Time
+	var startTimeAny any = startTimeGeneric
+	if t, ok := startTimeAny.(time.Time); ok {
+		start = t
 	} else {
-		// use start string create code
-		startStr = startInf.(string)
-		start, _ = time.ParseInLocation(format, startStr, time.Local)
-		startStr = start.Format(format)
+		var err error
+		start, err = time.ParseInLocation(format, startTimeAny.(string), time.Local)
+		if err != nil {
+			return "" // return an invalid code because the "parse" failed
+		}
 	}
+	startStr := start.Format(format)
+	end := start.Add(time.Minute * time.Duration(minutes))
 
-	end = start.Add(time.Minute * time.Duration(minutes))
-	endStr = end.Format(format)
-
-	// create sha1 encode string
-	sh := sha1.New()
-	_, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes)))
-	encoded := hex.EncodeToString(sh.Sum(nil))
+	if h == nil {
+		h = hmac.New(sha1.New, setting.GetGeneralTokenSigningSecret())
+	}
+	_, _ = fmt.Fprintf(h, "%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, end.Format(format), minutes)
+	encoded := hex.EncodeToString(h.Sum(nil))
 
 	code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
+	if len(code) != TimeLimitCodeLength {
+		panic("there is a hard requirement for the length of time-limited code") // it shouldn't happen
+	}
 	return code
 }
 
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index f21b89c74c..62de7229ac 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -4,20 +4,18 @@
 package base
 
 import (
+	"crypto/sha1"
+	"fmt"
 	"os"
 	"testing"
 	"time"
 
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
 	"github.com/stretchr/testify/assert"
 )
 
-func TestEncodeSha1(t *testing.T) {
-	assert.Equal(t,
-		"8843d7f92416211de9ebb963ff4ce28125932878",
-		EncodeSha1("foobar"),
-	)
-}
-
 func TestEncodeSha256(t *testing.T) {
 	assert.Equal(t,
 		"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2",
@@ -46,43 +44,54 @@ func TestBasicAuthDecode(t *testing.T) {
 }
 
 func TestVerifyTimeLimitCode(t *testing.T) {
-	tc := []struct {
-		data    string
-		minutes int
-		code    string
-		valid   bool
-	}{{
-		data:    "data",
-		minutes: 2,
-		code:    testCreateTimeLimitCode(t, "data", 2),
-		valid:   true,
-	}, {
-		data:    "abc123-ß",
-		minutes: 1,
-		code:    testCreateTimeLimitCode(t, "abc123-ß", 1),
-		valid:   true,
-	}, {
-		data:    "data",
-		minutes: 2,
-		code:    "2021012723240000005928251dac409d2c33a6eb82c63410aaad569bed",
-		valid:   false,
-	}}
-	for _, test := range tc {
-		actualValid := VerifyTimeLimitCode(test.data, test.minutes, test.code)
-		assert.Equal(t, test.valid, actualValid, "data: '%s' code: '%s' should be valid: %t", test.data, test.code, test.valid)
+	defer test.MockVariableValue(&setting.InstallLock, true)()
+	initGeneralSecret := func(secret string) {
+		setting.InstallLock = true
+		setting.CfgProvider, _ = setting.NewConfigProviderFromData(fmt.Sprintf(`
+[oauth2]
+JWT_SECRET = %s
+`, secret))
+		setting.LoadCommonSettings()
 	}
-}
 
-func testCreateTimeLimitCode(t *testing.T, data string, m int) string {
-	result0 := CreateTimeLimitCode(data, m, nil)
-	result1 := CreateTimeLimitCode(data, m, time.Now().Format("200601021504"))
-	result2 := CreateTimeLimitCode(data, m, time.Unix(time.Now().Unix()+int64(time.Minute)*int64(m), 0).Format("200601021504"))
+	initGeneralSecret("KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
+	now := time.Now()
 
-	assert.Equal(t, result0, result1)
-	assert.NotEqual(t, result0, result2)
+	t.Run("TestGenericParameter", func(t *testing.T) {
+		time2000 := time.Date(2000, 1, 2, 3, 4, 5, 0, time.Local)
+		assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, time2000, sha1.New()))
+		assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, "200001020304", sha1.New()))
+		assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, time2000, nil))
+		assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, "200001020304", nil))
+	})
 
-	assert.True(t, len(result0) != 0)
-	return result0
+	t.Run("TestInvalidCode", func(t *testing.T) {
+		assert.False(t, VerifyTimeLimitCode(now, "data", 2, ""))
+		assert.False(t, VerifyTimeLimitCode(now, "data", 2, "invalid code"))
+	})
+
+	t.Run("TestCreateAndVerify", func(t *testing.T) {
+		code := CreateTimeLimitCode("data", 2, now, nil)
+		assert.False(t, VerifyTimeLimitCode(now.Add(-time.Minute), "data", 2, code)) // not started yet
+		assert.True(t, VerifyTimeLimitCode(now, "data", 2, code))
+		assert.True(t, VerifyTimeLimitCode(now.Add(time.Minute), "data", 2, code))
+		assert.False(t, VerifyTimeLimitCode(now.Add(time.Minute), "DATA", 2, code))   // invalid data
+		assert.False(t, VerifyTimeLimitCode(now.Add(2*time.Minute), "data", 2, code)) // expired
+	})
+
+	t.Run("TestDifferentSecret", func(t *testing.T) {
+		// use another secret to ensure the code is invalid for different secret
+		verifyDataCode := func(c string) bool {
+			return VerifyTimeLimitCode(now, "data", 2, c)
+		}
+		code1 := CreateTimeLimitCode("data", 2, now, sha1.New())
+		code2 := CreateTimeLimitCode("data", 2, now, nil)
+		assert.True(t, verifyDataCode(code1))
+		assert.True(t, verifyDataCode(code2))
+		initGeneralSecret("000_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
+		assert.False(t, verifyDataCode(code1))
+		assert.False(t, verifyDataCode(code2))
+	})
 }
 
 func TestFileSize(t *testing.T) {
diff --git a/modules/git/utils.go b/modules/git/utils.go
index 0d67412707..53211c6451 100644
--- a/modules/git/utils.go
+++ b/modules/git/utils.go
@@ -4,6 +4,8 @@
 package git
 
 import (
+	"crypto/sha1"
+	"encoding/hex"
 	"fmt"
 	"io"
 	"os"
@@ -128,3 +130,9 @@ func (l *LimitedReaderCloser) Read(p []byte) (n int, err error) {
 func (l *LimitedReaderCloser) Close() error {
 	return l.C.Close()
 }
+
+func HashFilePathForWebUI(s string) string {
+	h := sha1.New()
+	_, _ = h.Write([]byte(s))
+	return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/modules/git/utils_test.go b/modules/git/utils_test.go
new file mode 100644
index 0000000000..1291cee637
--- /dev/null
+++ b/modules/git/utils_test.go
@@ -0,0 +1,17 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestHashFilePathForWebUI(t *testing.T) {
+	assert.Equal(t,
+		"8843d7f92416211de9ebb963ff4ce28125932878",
+		HashFilePathForWebUI("foobar"),
+	)
+}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 8c0fee71a0..818dc4d50f 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -931,7 +931,7 @@ func ExcerptBlob(ctx *context.Context) {
 		}
 	}
 	ctx.Data["section"] = section
-	ctx.Data["FileNameHash"] = base.EncodeSha1(filePath)
+	ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath)
 	ctx.Data["AfterCommitID"] = commitID
 	ctx.Data["Anchor"] = anchor
 	ctx.HTML(http.StatusOK, tplBlobExcerpt)
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 3a35d24dff..063c995d52 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -23,7 +23,6 @@ import (
 	pull_model "code.gitea.io/gitea/models/pull"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/analyze"
-	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/charset"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/highlight"
@@ -746,7 +745,7 @@ parsingLoop:
 	diffLineTypeBuffers[DiffLineAdd] = new(bytes.Buffer)
 	diffLineTypeBuffers[DiffLineDel] = new(bytes.Buffer)
 	for _, f := range diff.Files {
-		f.NameHash = base.EncodeSha1(f.Name)
+		f.NameHash = git.HashFilePathForWebUI(f.Name)
 
 		for _, buffer := range diffLineTypeBuffers {
 			buffer.Reset()

From ba83d27ab037a3dbcb0c84b731c8030e9bdc26a8 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 21 May 2024 00:26:00 +0000
Subject: [PATCH 023/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_ja-JP.ini | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index cf9d9bbc51..66dedcbb51 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -436,6 +436,7 @@ oauth_signin_submit=アカウントにリンク
 oauth.signin.error=認可リクエストの処理中にエラーが発生しました。このエラーが解決しない場合は、サイト管理者に問い合わせてください。
 oauth.signin.error.access_denied=認可リクエストが拒否されました。
 oauth.signin.error.temporarily_unavailable=認証サーバーが一時的に利用できないため、認可に失敗しました。後でもう一度やり直してください。
+oauth_callback_unable_auto_reg=自動登録が有効になっていますが、OAuth2プロバイダー %[1]s の応答はフィールド %[2]s が不足しており、自動でアカウントを作成することができません。 アカウントを作成またはリンクするか、サイト管理者に問い合わせてください。
 openid_connect_submit=接続
 openid_connect_title=既存のアカウントに接続
 openid_connect_desc=選択したOpenID URIは未登録です。 ここで新しいアカウントと関連付けます。
@@ -763,6 +764,8 @@ manage_themes=デフォルトのテーマを選択
 manage_openid=OpenIDアドレスの管理
 email_desc=プライマリメールアドレスは、通知、パスワードの回復、さらにメールアドレスを隠さない場合は、WebベースのGit操作にも使用されます。
 theme_desc=この設定がサイト全体のデフォルトのテーマとなります。
+theme_colorblindness_help=色覚障害テーマのサポート
+theme_colorblindness_prompt=Giteaには基本的な色覚障害サポートを含むテーマがいくつか入っていますが、それらは色定義が少ししかありません。 作業はまだ進行中です。 テーマCSSファイルにもっと多くの色を定義していくことで、さらに改善できる余地があります。
 primary=プライマリー
 activated=アクティベート済み
 requires_activation=アクティベーションが必要
@@ -3317,6 +3320,7 @@ self_check.database_collation_case_insensitive=データベースは照合順序
 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を手で実行するしかありません。
+self_check.location_origin_mismatch=現在のURL (%[1]s) は、Giteaが見ているURL (%[2]s) に一致していません。 リバースプロキシを使用している場合は、"Host" ヘッダーと "X-Forwarded-Proto" ヘッダーが正しく設定されていることを確認してください。
 
 [action]
 create_repo=がリポジトリ <a href="%s">%s</a> を作成しました
@@ -3344,6 +3348,7 @@ mirror_sync_create=が <a href="%[1]s">%[4]s</a> の新しい参照 <a href="%[2
 mirror_sync_delete=が <a href="%[1]s">%[3]s</a> の参照 <code>%[2]s</code> をミラーから反映し、削除しました
 approve_pull_request=`が <a href="%[1]s">%[3]s#%[2]s</a> を承認しました`
 reject_pull_request=`が <a href="%[1]s">%[3]s#%[2]s</a>について変更を提案しました`
+publish_release=`が <a href="%[1]s">%[3]s</a> の <a href="%[2]s">%[4]s</a> をリリースしました`
 review_dismissed=`が <b>%[4]s</b> の <a href="%[1]s">%[3]s#%[2]s</a> へのレビューを棄却しました`
 review_dismissed_reason=理由:
 create_branch=がブランチ <a href="%[2]s">%[3]s</a> を <a href="%[1]s">%[4]s</a> に作成しました

From 1007ce764ea80b48120b796175d7d1210cbb6f74 Mon Sep 17 00:00:00 2001
From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
Date: Mon, 20 May 2024 19:23:07 -0700
Subject: [PATCH 024/131] Don't include link of deleted branch when listing
 branches (#31028)

From
https://github.com/go-gitea/gitea/issues/31018#issuecomment-2119622680.

This commit removes the link to a deleted branch name because it returns
a 404 while it is in this deleted state. GitHub also throws a 404 when
navigating to a branch link that was just deleted, but this deleted
branch is removed from the branch list after a page refresh. Since with
Gitea this deleted branch would be kept around for quite some time
(well, until the "cleanup deleted branches" cron job begins), it makes
sense to not have this as a link that users can navigate to.
---
 templates/repo/branch/list.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 77cccd65b7..dcfe082276 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -87,7 +87,7 @@
 							<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>
+									<span class="gt-ellipsis">{{.DBBranch.Name}}</span>
 									<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>

From c6cf96d31d80ab79d370a6192fd761b4443daec2 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 21 May 2024 23:23:22 +0800
Subject: [PATCH 025/131] Fix automerge will not work because of some events
 haven't been triggered (#30780)

Replace #25741
Close #24445
Close #30658
Close #20646
~Depends on #30805~

Since #25741 has been rewritten totally, to make the contribution
easier, I will continue the work in this PR. Thanks @6543

---------

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/issues/review.go                       |   6 +-
 services/automerge/automerge.go               | 108 ++++++----
 services/automerge/notify.go                  |  46 ++++
 .../repository/commitstatus/commitstatus.go   |   2 +-
 tests/integration/editor_test.go              |  41 ++--
 tests/integration/pull_merge_test.go          | 198 ++++++++++++++++++
 tests/integration/pull_review_test.go         |  12 +-
 7 files changed, 347 insertions(+), 66 deletions(-)
 create mode 100644 services/automerge/notify.go

diff --git a/models/issues/review.go b/models/issues/review.go
index 3c6934b060..ca6fd6035b 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -155,14 +155,14 @@ func (r *Review) LoadCodeComments(ctx context.Context) (err error) {
 	if r.CodeComments != nil {
 		return err
 	}
-	if err = r.loadIssue(ctx); err != nil {
+	if err = r.LoadIssue(ctx); err != nil {
 		return err
 	}
 	r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false)
 	return err
 }
 
-func (r *Review) loadIssue(ctx context.Context) (err error) {
+func (r *Review) LoadIssue(ctx context.Context) (err error) {
 	if r.Issue != nil {
 		return err
 	}
@@ -199,7 +199,7 @@ func (r *Review) LoadReviewerTeam(ctx context.Context) (err error) {
 
 // LoadAttributes loads all attributes except CodeComments
 func (r *Review) LoadAttributes(ctx context.Context) (err error) {
-	if err = r.loadIssue(ctx); err != nil {
+	if err = r.LoadIssue(ctx); err != nil {
 		return err
 	}
 	if err = r.LoadCodeComments(ctx); err != nil {
diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go
index bd1317c7f4..10f3c28d56 100644
--- a/services/automerge/automerge.go
+++ b/services/automerge/automerge.go
@@ -22,6 +22,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/queue"
+	notify_service "code.gitea.io/gitea/services/notify"
 	pull_service "code.gitea.io/gitea/services/pull"
 )
 
@@ -30,6 +31,8 @@ var prAutoMergeQueue *queue.WorkerPoolQueue[string]
 
 // Init runs the task queue to that handles auto merges
 func Init() error {
+	notify_service.RegisterNotifier(NewNotifier())
+
 	prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
 	if prAutoMergeQueue == nil {
 		return fmt.Errorf("unable to create pr_auto_merge queue")
@@ -47,7 +50,7 @@ func handler(items ...string) []string {
 			log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err)
 			continue
 		}
-		handlePull(id, sha)
+		handlePullRequestAutoMerge(id, sha)
 	}
 	return nil
 }
@@ -62,16 +65,6 @@ func addToQueue(pr *issues_model.PullRequest, sha string) {
 // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
 func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
 	err = db.WithTx(ctx, func(ctx context.Context) error {
-		lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull)
-		if err != nil {
-			return err
-		}
-
-		// we don't need to schedule
-		if lastCommitStatus.IsSuccess() {
-			return nil
-		}
-
 		if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil {
 			return err
 		}
@@ -95,8 +88,8 @@ func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *
 	})
 }
 
-// MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded
-func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error {
+// StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA
+func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error {
 	pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
 		return !pr.HasMerged && pr.CanAutoMerge()
 	})
@@ -111,6 +104,32 @@ func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model
 	return nil
 }
 
+// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
+func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
+	if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
+		return
+	}
+
+	if err := pull.LoadBaseRepo(ctx); err != nil {
+		log.Error("LoadBaseRepo: %v", err)
+		return
+	}
+
+	gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
+	if err != nil {
+		log.Error("OpenRepository: %v", err)
+		return
+	}
+	defer gitRepo.Close()
+	commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
+	if err != nil {
+		log.Error("GetRefCommitID: %v", err)
+		return
+	}
+
+	addToQueue(pull, commitID)
+}
+
 func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
 	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
 	if err != nil {
@@ -161,7 +180,8 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.
 	return pulls, nil
 }
 
-func handlePull(pullID int64, sha string) {
+// handlePullRequestAutoMerge merge the pull request if all checks are successful
+func handlePullRequestAutoMerge(pullID int64, sha string) {
 	ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
 		fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha))
 	defer finished()
@@ -182,24 +202,50 @@ func handlePull(pullID int64, sha string) {
 		return
 	}
 
+	if err = pr.LoadBaseRepo(ctx); err != nil {
+		log.Error("%-v LoadBaseRepo: %v", pr, err)
+		return
+	}
+
+	// check the sha is the same as pull request head commit id
+	baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+	if err != nil {
+		log.Error("OpenRepository: %v", err)
+		return
+	}
+	defer baseGitRepo.Close()
+
+	headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+	if err != nil {
+		log.Error("GetRefCommitID: %v", err)
+		return
+	}
+	if headCommitID != sha {
+		log.Warn("Head commit id of auto merge %-v does not match sha [%s], it may means the head branch has been updated. Just ignore this request because a new request expected in the queue", pr, sha)
+		return
+	}
+
 	// Get all checks for this pr
 	// We get the latest sha commit hash again to handle the case where the check of a previous push
 	// did not succeed or was not finished yet.
-
 	if err = pr.LoadHeadRepo(ctx); err != nil {
 		log.Error("%-v LoadHeadRepo: %v", pr, err)
 		return
 	}
 
-	headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo)
-	if err != nil {
-		log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
-		return
+	var headGitRepo *git.Repository
+	if pr.BaseRepoID == pr.HeadRepoID {
+		headGitRepo = baseGitRepo
+	} else {
+		headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
+		if err != nil {
+			log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
+			return
+		}
+		defer headGitRepo.Close()
 	}
-	defer headGitRepo.Close()
 
 	headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch)
-
 	if pr.HeadRepo == nil || !headBranchExist {
 		log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch)
 		return
@@ -238,25 +284,11 @@ func handlePull(pullID int64, sha string) {
 		return
 	}
 
-	var baseGitRepo *git.Repository
-	if pr.BaseRepoID == pr.HeadRepoID {
-		baseGitRepo = headGitRepo
-	} else {
-		if err = pr.LoadBaseRepo(ctx); err != nil {
-			log.Error("%-v LoadBaseRepo: %v", pr, err)
-			return
-		}
-
-		baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
-		if err != nil {
-			log.Error("OpenRepository %-v: %v", pr.BaseRepo, err)
-			return
-		}
-		defer baseGitRepo.Close()
-	}
-
 	if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
 		log.Error("pull_service.Merge: %v", err)
+		// FIXME: if merge failed, we should display some error message to the pull request page.
+		// The resolution is add a new column on automerge table named `error_message` to store the error message and displayed
+		// on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch.
 		return
 	}
 }
diff --git a/services/automerge/notify.go b/services/automerge/notify.go
new file mode 100644
index 0000000000..cb078214f6
--- /dev/null
+++ b/services/automerge/notify.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package automerge
+
+import (
+	"context"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
+	notify_service "code.gitea.io/gitea/services/notify"
+)
+
+type automergeNotifier struct {
+	notify_service.NullNotifier
+}
+
+var _ notify_service.Notifier = &automergeNotifier{}
+
+// NewNotifier create a new automergeNotifier notifier
+func NewNotifier() notify_service.Notifier {
+	return &automergeNotifier{}
+}
+
+func (n *automergeNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
+	// as a missing / blocking reviews could have blocked a pending automerge let's recheck
+	if review.Type == issues_model.ReviewTypeApprove {
+		if err := StartPRCheckAndAutoMergeBySHA(ctx, review.CommitID, pr.BaseRepo); err != nil {
+			log.Error("StartPullRequestAutoMergeCheckBySHA: %v", err)
+		}
+	}
+}
+
+func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
+	if err := review.LoadIssue(ctx); err != nil {
+		log.Error("LoadIssue: %v", err)
+		return
+	}
+	if err := review.Issue.LoadPullRequest(ctx); err != nil {
+		log.Error("LoadPullRequest: %v", err)
+		return
+	}
+	// as reviews could have blocked a pending automerge let's recheck
+	StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
+}
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
index 444ae04d0c..adc59abed8 100644
--- a/services/repository/commitstatus/commitstatus.go
+++ b/services/repository/commitstatus/commitstatus.go
@@ -115,7 +115,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
 	}
 
 	if status.State.IsSuccess() {
-		if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
+		if err := automerge.StartPRCheckAndAutoMergeBySHA(ctx, sha, repo); err != nil {
 			return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
 		}
 	}
diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go
index 045567ce77..f510c79bc6 100644
--- a/tests/integration/editor_test.go
+++ b/tests/integration/editor_test.go
@@ -4,6 +4,7 @@
 package integration
 
 import (
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -19,27 +20,31 @@ import (
 func TestCreateFile(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user2")
-
-		// Request editor page
-		req := NewRequest(t, "GET", "/user2/repo1/_new/master/")
-		resp := session.MakeRequest(t, req, http.StatusOK)
-
-		doc := NewHTMLParser(t, resp.Body)
-		lastCommit := doc.GetInputValueByName("last_commit")
-		assert.NotEmpty(t, lastCommit)
-
-		// Save new file to master branch
-		req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
-			"_csrf":         doc.GetCSRF(),
-			"last_commit":   lastCommit,
-			"tree_path":     "test.txt",
-			"content":       "Content",
-			"commit_choice": "direct",
-		})
-		session.MakeRequest(t, req, http.StatusSeeOther)
+		testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
 	})
 }
 
+func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder {
+	// Request editor page
+	newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
+	req := NewRequest(t, "GET", newURL)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	doc := NewHTMLParser(t, resp.Body)
+	lastCommit := doc.GetInputValueByName("last_commit")
+	assert.NotEmpty(t, lastCommit)
+
+	// Save new file to master branch
+	req = NewRequestWithValues(t, "POST", newURL, map[string]string{
+		"_csrf":         doc.GetCSRF(),
+		"last_commit":   lastCommit,
+		"tree_path":     filePath,
+		"content":       content,
+		"commit_choice": "direct",
+	})
+	return session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
 func TestCreateFileOnProtectedBranch(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user2")
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index 826568caf2..979c408388 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -12,6 +12,8 @@ import (
 	"net/url"
 	"os"
 	"path"
+	"path/filepath"
+	"strconv"
 	"strings"
 	"testing"
 	"time"
@@ -19,7 +21,9 @@ import (
 	"code.gitea.io/gitea/models"
 	auth_model "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"
+	pull_model "code.gitea.io/gitea/models/pull"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -30,8 +34,10 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
+	"code.gitea.io/gitea/services/automerge"
 	"code.gitea.io/gitea/services/pull"
 	repo_service "code.gitea.io/gitea/services/repository"
+	commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
 	files_service "code.gitea.io/gitea/services/repository/files"
 
 	"github.com/stretchr/testify/assert"
@@ -648,3 +654,195 @@ func TestPullMergeIndexerNotifier(t *testing.T) {
 		}
 	})
 }
+
+func testResetRepo(t *testing.T, repoPath, branch, commitID string) {
+	f, err := os.OpenFile(filepath.Join(repoPath, "refs", "heads", branch), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
+	assert.NoError(t, err)
+	_, err = f.WriteString(commitID + "\n")
+	assert.NoError(t, err)
+	f.Close()
+
+	repo, err := git.OpenRepository(context.Background(), repoPath)
+	assert.NoError(t, err)
+	defer repo.Close()
+	id, err := repo.GetBranchCommitID(branch)
+	assert.NoError(t, err)
+	assert.EqualValues(t, commitID, id)
+}
+
+func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// create a pull request
+		session := loginUser(t, "user1")
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+		forkedName := "repo1-1"
+		testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
+		defer func() {
+			testDeleteRepository(t, session, "user1", forkedName)
+		}()
+		testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
+		testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
+
+		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
+		forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+			BaseRepoID: baseRepo.ID,
+			BaseBranch: "master",
+			HeadRepoID: forkedRepo.ID,
+			HeadBranch: "master",
+		})
+
+		// add protected branch for commit status
+		csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
+		// Change master branch to protected
+		req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
+			"_csrf":                 csrf,
+			"rule_name":             "master",
+			"enable_push":           "true",
+			"enable_status_check":   "true",
+			"status_check_contexts": "gitea/actions",
+		})
+		session.MakeRequest(t, req, http.StatusSeeOther)
+
+		// first time insert automerge record, return true
+		scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
+		assert.NoError(t, err)
+		assert.True(t, scheduled)
+
+		// second time insert automerge record, return false because it does exist
+		scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
+		assert.Error(t, err)
+		assert.False(t, scheduled)
+
+		// reload pr again
+		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+		assert.False(t, pr.HasMerged)
+		assert.Empty(t, pr.MergedCommitID)
+
+		// update commit status to success, then it should be merged automatically
+		baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
+		assert.NoError(t, err)
+		sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+		assert.NoError(t, err)
+		masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
+		assert.NoError(t, err)
+
+		branches, _, err := baseGitRepo.GetBranchNames(0, 100)
+		assert.NoError(t, err)
+		assert.ElementsMatch(t, []string{"sub-home-md-img-check", "home-md-img-check", "pr-to-update", "branch2", "DefaultBranch", "develop", "feature/1", "master"}, branches)
+		baseGitRepo.Close()
+		defer func() {
+			testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
+		}()
+
+		err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
+			State:     api.CommitStatusSuccess,
+			TargetURL: "https://gitea.com",
+			Context:   "gitea/actions",
+		})
+		assert.NoError(t, err)
+
+		time.Sleep(2 * time.Second)
+
+		// realod pr again
+		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+		assert.True(t, pr.HasMerged)
+		assert.NotEmpty(t, pr.MergedCommitID)
+
+		unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
+	})
+}
+
+func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// create a pull request
+		session := loginUser(t, "user1")
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+		forkedName := "repo1-2"
+		testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
+		defer func() {
+			testDeleteRepository(t, session, "user1", forkedName)
+		}()
+		testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
+		testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
+
+		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
+		forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
+		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+			BaseRepoID: baseRepo.ID,
+			BaseBranch: "master",
+			HeadRepoID: forkedRepo.ID,
+			HeadBranch: "master",
+		})
+
+		// add protected branch for commit status
+		csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
+		// Change master branch to protected
+		req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
+			"_csrf":                 csrf,
+			"rule_name":             "master",
+			"enable_push":           "true",
+			"enable_status_check":   "true",
+			"status_check_contexts": "gitea/actions",
+			"required_approvals":    "1",
+		})
+		session.MakeRequest(t, req, http.StatusSeeOther)
+
+		// first time insert automerge record, return true
+		scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
+		assert.NoError(t, err)
+		assert.True(t, scheduled)
+
+		// second time insert automerge record, return false because it does exist
+		scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
+		assert.Error(t, err)
+		assert.False(t, scheduled)
+
+		// reload pr again
+		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+		assert.False(t, pr.HasMerged)
+		assert.Empty(t, pr.MergedCommitID)
+
+		// update commit status to success, then it should be merged automatically
+		baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
+		assert.NoError(t, err)
+		sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+		assert.NoError(t, err)
+		masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
+		assert.NoError(t, err)
+		baseGitRepo.Close()
+		defer func() {
+			testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
+		}()
+
+		err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
+			State:     api.CommitStatusSuccess,
+			TargetURL: "https://gitea.com",
+			Context:   "gitea/actions",
+		})
+		assert.NoError(t, err)
+
+		time.Sleep(2 * time.Second)
+
+		// reload pr again
+		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+		assert.False(t, pr.HasMerged)
+		assert.Empty(t, pr.MergedCommitID)
+
+		// approve the PR from non-author
+		approveSession := loginUser(t, "user2")
+		req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
+		resp := approveSession.MakeRequest(t, req, http.StatusOK)
+		htmlDoc := NewHTMLParser(t, resp.Body)
+		testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
+
+		time.Sleep(2 * time.Second)
+
+		// realod pr again
+		pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
+		assert.True(t, pr.HasMerged)
+		assert.NotEmpty(t, pr.MergedCommitID)
+
+		unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
+	})
+}
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 273332a36b..df5d7b38ea 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -202,10 +202,10 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
 			htmlDoc := NewHTMLParser(t, resp.Body)
 
 			// Submit an approve review on the PR.
-			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
+			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "", "approve", http.StatusUnprocessableEntity)
 
 			// Submit a reject review on the PR.
-			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
+			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "", "reject", http.StatusUnprocessableEntity)
 		})
 
 		t.Run("Submit approve/reject review on closed PR", func(t *testing.T) {
@@ -222,18 +222,18 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
 			htmlDoc := NewHTMLParser(t, resp.Body)
 
 			// Submit an approve review on the PR.
-			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
+			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "", "approve", http.StatusUnprocessableEntity)
 
 			// Submit a reject review on the PR.
-			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
+			testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "", "reject", http.StatusUnprocessableEntity)
 		})
 	})
 }
 
-func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pullNumber, reviewType string, expectedSubmitStatus int) *httptest.ResponseRecorder {
+func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pullNumber, commitID, reviewType string, expectedSubmitStatus int) *httptest.ResponseRecorder {
 	options := map[string]string{
 		"_csrf":     csrf,
-		"commit_id": "",
+		"commit_id": commitID,
 		"content":   "test",
 		"type":      reviewType,
 	}

From 9c8c9ff6d10b35de8d2d7eae0fc2646ad9bbe94a Mon Sep 17 00:00:00 2001
From: Denys Konovalov <kontakt@denyskon.de>
Date: Tue, 21 May 2024 18:23:49 +0200
Subject: [PATCH 026/131] use existing oauth grant for public client (#31015)

Do not try to create a new authorization grant when one exists already,
thus preventing a DB-related authorization issue.

Fix https://github.com/go-gitea/gitea/pull/30790#issuecomment-2118812426

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 routers/web/auth/oauth.go | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 84fa473044..b337b6b156 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -556,15 +556,30 @@ func GrantApplicationOAuth(ctx *context.Context) {
 		ctx.ServerError("GetOAuth2ApplicationByClientID", err)
 		return
 	}
-	grant, err := app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
+	grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
 	if err != nil {
+		handleServerError(ctx, form.State, form.RedirectURI)
+		return
+	}
+	if grant == nil {
+		grant, err = app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
+		if err != nil {
+			handleAuthorizeError(ctx, AuthorizeError{
+				State:            form.State,
+				ErrorDescription: "cannot create grant for user",
+				ErrorCode:        ErrorCodeServerError,
+			}, form.RedirectURI)
+			return
+		}
+	} else if grant.Scope != form.Scope {
 		handleAuthorizeError(ctx, AuthorizeError{
 			State:            form.State,
-			ErrorDescription: "cannot create grant for user",
+			ErrorDescription: "a grant exists with different scope",
 			ErrorCode:        ErrorCodeServerError,
 		}, form.RedirectURI)
 		return
 	}
+
 	if len(form.Nonce) > 0 {
 		err := grant.SetNonce(ctx, form.Nonce)
 		if err != nil {

From daf2a4c047c88083d8820bdee9074357d5c5d7b7 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Wed, 22 May 2024 02:00:35 +0900
Subject: [PATCH 027/131] Fix wrong display of recently pushed notification
 (#25812)

There's a bug in #25715:
If user pushed a commit into another repo with same branch name, the
no-related repo will display the recently pushed notification
incorrectly.
It is simple to fix this, we should match the repo id in the sql query.


![image](https://github.com/go-gitea/gitea/assets/18380374/9411a926-16f1-419e-a1b5-e953af38bab1)
The latest commit is 2 weeks ago.

![image](https://github.com/go-gitea/gitea/assets/18380374/52f9ab22-4999-43ac-a86f-6d36fb1e0411)

The notification comes from another repo with same branch name:

![image](https://github.com/go-gitea/gitea/assets/18380374/a26bc335-8e5b-4b9c-a965-c3dc3fa6f252)


After:
In forked repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/ce6ffc35-deb7-4be7-8b09-184207392f32)
New PR Link will redirect to the original repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/7b98e76f-0c75-494c-9462-80cf9f98e786)
In the original repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/5f6a821b-e51a-4bbd-9980-d9eb94a3c847)
New PR Link:

![image](https://github.com/go-gitea/gitea/assets/18380374/1ce8c879-9f11-4312-8c32-695d7d9af0df)

In the same repo:

![image](https://github.com/go-gitea/gitea/assets/18380374/64b56073-4d0e-40c4-b8a0-80be7a775f69)
New PR Link:

![image](https://github.com/go-gitea/gitea/assets/18380374/96e1b6a3-fb98-40ee-b2ee-648039fb0dcf)

08/15 Update:
Follow #26257, added permission check and logic fix mentioned in
https://github.com/go-gitea/gitea/pull/26257#discussion_r1294085203


2024/04/25 Update:
Fix #30611

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/fixtures/branch.yml                    |  36 +++++
 models/fixtures/issue_index.yml               |   8 +
 models/fixtures/org_user.yml                  |  12 ++
 models/fixtures/repository.yml                |   2 +-
 models/fixtures/team.yml                      |  22 +++
 models/fixtures/team_unit.yml                 |  18 +++
 models/fixtures/team_user.yml                 |  12 ++
 models/fixtures/user.yml                      |   8 +-
 models/git/branch.go                          | 142 ++++++++++++++---
 models/git/branch_list.go                     |  19 +++
 models/organization/org_user_test.go          |   6 +-
 models/repo/repo_list.go                      |   6 +
 routers/web/repo/view.go                      |  26 ++-
 .../code/recently_pushed_new_branches.tmpl    |   4 +-
 tests/integration/api_user_orgs_test.go       |  26 +++
 tests/integration/compare_test.go             |   2 +-
 tests/integration/empty_repo_test.go          |  13 ++
 tests/integration/integration_test.go         |   9 ++
 tests/integration/pull_compare_test.go        |   2 +-
 tests/integration/pull_create_test.go         |   6 +-
 tests/integration/pull_merge_test.go          |  30 ++--
 tests/integration/pull_review_test.go         |   2 +-
 tests/integration/pull_status_test.go         |   6 +-
 tests/integration/repo_activity_test.go       |   2 +-
 tests/integration/repo_branch_test.go         | 148 +++++++++++++++++-
 tests/integration/repo_fork_test.go           |  13 +-
 26 files changed, 508 insertions(+), 72 deletions(-)

diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml
index 93003049c6..c7bdff7733 100644
--- a/models/fixtures/branch.yml
+++ b/models/fixtures/branch.yml
@@ -45,3 +45,39 @@
   is_deleted: false
   deleted_by_id: 0
   deleted_unix: 0
+
+-
+  id: 5
+  repo_id: 10
+  name: 'master'
+  commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+  commit_message: 'Initial commit'
+  commit_time: 1489927679
+  pusher_id: 12
+  is_deleted: false
+  deleted_by_id: 0
+  deleted_unix: 0
+
+-
+  id: 6
+  repo_id: 10
+  name: 'outdated-new-branch'
+  commit_id: 'cb24c347e328d83c1e0c3c908a6b2c0a2fcb8a3d'
+  commit_message: 'add'
+  commit_time: 1489927679
+  pusher_id: 12
+  is_deleted: false
+  deleted_by_id: 0
+  deleted_unix: 0
+
+-
+  id: 14
+  repo_id: 11
+  name: 'master'
+  commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+  commit_message: 'Initial commit'
+  commit_time: 1489927679
+  pusher_id: 13
+  is_deleted: false
+  deleted_by_id: 0
+  deleted_unix: 0
diff --git a/models/fixtures/issue_index.yml b/models/fixtures/issue_index.yml
index de6e955804..5aabc08e38 100644
--- a/models/fixtures/issue_index.yml
+++ b/models/fixtures/issue_index.yml
@@ -1,27 +1,35 @@
 -
   group_id: 1
   max_index: 5
+
 -
   group_id: 2
   max_index: 2
+
 -
   group_id: 3
   max_index: 2
+
 -
   group_id: 10
   max_index: 1
+
 -
   group_id: 32
   max_index: 2
+
 -
   group_id: 48
   max_index: 1
+
 -
   group_id: 42
   max_index: 1
+
 -
   group_id: 50
   max_index: 1
+
 -
   group_id: 51
   max_index: 1
diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml
index a7fbcb2c5a..cf21b84aa9 100644
--- a/models/fixtures/org_user.yml
+++ b/models/fixtures/org_user.yml
@@ -117,3 +117,15 @@
   uid: 40
   org_id: 41
   is_public: true
+
+-
+  id: 21
+  uid: 12
+  org_id: 25
+  is_public: true
+
+-
+  id: 22
+  uid: 2
+  org_id: 35
+  is_public: true
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index e5c6224c96..e1f1dd7367 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -327,7 +327,7 @@
   is_archived: false
   is_mirror: false
   status: 0
-  is_fork: false
+  is_fork: true
   fork_id: 10
   is_template: false
   template_id: 0
diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml
index 149fe90888..b549d0589b 100644
--- a/models/fixtures/team.yml
+++ b/models/fixtures/team.yml
@@ -239,3 +239,25 @@
   num_members: 2
   includes_all_repositories: false
   can_create_org_repo: false
+
+-
+  id: 23
+  org_id: 25
+  lower_name: owners
+  name: Owners
+  authorize: 4 # owner
+  num_repos: 0
+  num_members: 1
+  includes_all_repositories: false
+  can_create_org_repo: true
+
+-
+  id: 24
+  org_id: 35
+  lower_name: team24
+  name: team24
+  authorize: 2 # write
+  num_repos: 0
+  num_members: 1
+  includes_all_repositories: true
+  can_create_org_repo: false
diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml
index de0e8d738b..110019eee3 100644
--- a/models/fixtures/team_unit.yml
+++ b/models/fixtures/team_unit.yml
@@ -322,3 +322,21 @@
   team_id: 22
   type: 3
   access_mode: 1
+
+-
+  id: 55
+  team_id: 18
+  type: 1 # code
+  access_mode: 4
+
+-
+  id: 56
+  team_id: 23
+  type: 1 # code
+  access_mode: 4
+
+-
+  id: 57
+  team_id: 24
+  type: 1 # code
+  access_mode: 2
diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml
index 02d57ae644..6b2d153278 100644
--- a/models/fixtures/team_user.yml
+++ b/models/fixtures/team_user.yml
@@ -147,3 +147,15 @@
   org_id: 41
   team_id: 22
   uid: 39
+
+-
+  id: 26
+  org_id: 25
+  team_id: 23
+  uid: 12
+
+-
+  id: 27
+  org_id: 35
+  team_id: 24
+  uid: 2
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index a3de535508..8504d88ce5 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -918,8 +918,8 @@
   num_following: 0
   num_stars: 0
   num_repos: 0
-  num_teams: 1
-  num_members: 1
+  num_teams: 2
+  num_members: 2
   visibility: 0
   repo_admin_change_team_access: false
   theme: ""
@@ -1289,8 +1289,8 @@
   num_following: 0
   num_stars: 0
   num_repos: 0
-  num_teams: 1
-  num_members: 1
+  num_teams: 2
+  num_members: 2
   visibility: 2
   repo_admin_change_team_access: false
   theme: ""
diff --git a/models/git/branch.go b/models/git/branch.go
index 2979dff3d2..c315d921ff 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -10,9 +10,11 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	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/git"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
@@ -102,8 +104,9 @@ func (err ErrBranchesEqual) Unwrap() error {
 // for pagination, keyword search and filtering
 type Branch struct {
 	ID            int64
-	RepoID        int64  `xorm:"UNIQUE(s)"`
-	Name          string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
+	RepoID        int64                  `xorm:"UNIQUE(s)"`
+	Repo          *repo_model.Repository `xorm:"-"`
+	Name          string                 `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment
 	CommitID      string
 	CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line)
 	PusherID      int64
@@ -139,6 +142,14 @@ func (b *Branch) LoadPusher(ctx context.Context) (err error) {
 	return err
 }
 
+func (b *Branch) LoadRepo(ctx context.Context) (err error) {
+	if b.Repo != nil || b.RepoID == 0 {
+		return nil
+	}
+	b.Repo, err = repo_model.GetRepositoryByID(ctx, b.RepoID)
+	return err
+}
+
 func init() {
 	db.RegisterModel(new(Branch))
 	db.RegisterModel(new(RenamedBranch))
@@ -400,24 +411,111 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
 	return committer.Commit()
 }
 
-// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 6 hours which has no opened PRs created
-// except the indicate branch
-func FindRecentlyPushedNewBranches(ctx context.Context, repoID, userID int64, excludeBranchName string) (BranchList, error) {
-	branches := make(BranchList, 0, 2)
-	subQuery := builder.Select("head_branch").From("pull_request").
-		InnerJoin("issue", "issue.id = pull_request.issue_id").
-		Where(builder.Eq{
-			"pull_request.head_repo_id": repoID,
-			"issue.is_closed":           false,
-		})
-	err := db.GetEngine(ctx).
-		Where("pusher_id=? AND is_deleted=?", userID, false).
-		And("name <> ?", excludeBranchName).
-		And("repo_id = ?", repoID).
-		And("commit_time >= ?", time.Now().Add(-time.Hour*6).Unix()).
-		NotIn("name", subQuery).
-		OrderBy("branch.commit_time DESC").
-		Limit(2).
-		Find(&branches)
-	return branches, err
+type FindRecentlyPushedNewBranchesOptions struct {
+	Repo            *repo_model.Repository
+	BaseRepo        *repo_model.Repository
+	CommitAfterUnix int64
+	MaxCount        int
+}
+
+type RecentlyPushedNewBranch struct {
+	BranchDisplayName string
+	BranchLink        string
+	BranchCompareURL  string
+	CommitTime        timeutil.TimeStamp
+}
+
+// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
+// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours
+// if opts.ListOptions is not set, we will only display top 2 latest branch
+func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
+	if doer == nil {
+		return []*RecentlyPushedNewBranch{}, nil
+	}
+
+	// find all related repo ids
+	repoOpts := repo_model.SearchRepoOptions{
+		Actor:      doer,
+		Private:    true,
+		AllPublic:  false, // Include also all public repositories of users and public organisations
+		AllLimited: false, // Include also all public repositories of limited organisations
+		Fork:       optional.Some(true),
+		ForkFrom:   opts.BaseRepo.ID,
+		Archived:   optional.Some(false),
+	}
+	repoCond := repo_model.SearchRepositoryCondition(&repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode))
+	if opts.Repo.ID == opts.BaseRepo.ID {
+		// should also include the base repo's branches
+		repoCond = repoCond.Or(builder.Eq{"id": opts.BaseRepo.ID})
+	} else {
+		// in fork repo, we only detect the fork repo's branch
+		repoCond = repoCond.And(builder.Eq{"id": opts.Repo.ID})
+	}
+	repoIDs := builder.Select("id").From("repository").Where(repoCond)
+
+	if opts.CommitAfterUnix == 0 {
+		opts.CommitAfterUnix = time.Now().Add(-time.Hour * 2).Unix()
+	}
+
+	baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch)
+	if err != nil {
+		return nil, err
+	}
+
+	// find all related branches, these branches may already created PRs, we will check later
+	var branches []*Branch
+	if err := db.GetEngine(ctx).
+		Where(builder.And(
+			builder.Eq{
+				"pusher_id":  doer.ID,
+				"is_deleted": false,
+			},
+			builder.Gte{"commit_time": opts.CommitAfterUnix},
+			builder.In("repo_id", repoIDs),
+			// newly created branch have no changes, so skip them
+			builder.Neq{"commit_id": baseBranch.CommitID},
+		)).
+		OrderBy(db.SearchOrderByRecentUpdated.String()).
+		Find(&branches); err != nil {
+		return nil, err
+	}
+
+	newBranches := make([]*RecentlyPushedNewBranch, 0, len(branches))
+	if opts.MaxCount == 0 {
+		// by default we display 2 recently pushed new branch
+		opts.MaxCount = 2
+	}
+	for _, branch := range branches {
+		// whether branch have already created PR
+		count, err := db.GetEngine(ctx).Table("pull_request").
+			// we should not only use branch name here, because if there are branches with same name in other repos,
+			// we can not detect them correctly
+			Where(builder.Eq{"head_repo_id": branch.RepoID, "head_branch": branch.Name}).Count()
+		if err != nil {
+			return nil, err
+		}
+
+		// if no PR, we add to the result
+		if count == 0 {
+			if err := branch.LoadRepo(ctx); err != nil {
+				return nil, err
+			}
+
+			branchDisplayName := branch.Name
+			if branch.Repo.ID != opts.BaseRepo.ID && branch.Repo.ID != opts.Repo.ID {
+				branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
+			}
+			newBranches = append(newBranches, &RecentlyPushedNewBranch{
+				BranchDisplayName: branchDisplayName,
+				BranchLink:        fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
+				BranchCompareURL:  branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
+				CommitTime:        branch.CommitTime,
+			})
+		}
+		if len(newBranches) == opts.MaxCount {
+			break
+		}
+	}
+
+	return newBranches, nil
 }
diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 980bd7b4c9..5c887461d5 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -7,6 +7,7 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models/db"
+	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/optional"
@@ -59,6 +60,24 @@ func (branches BranchList) LoadPusher(ctx context.Context) error {
 	return nil
 }
 
+func (branches BranchList) LoadRepo(ctx context.Context) error {
+	ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) {
+		return branch.RepoID, branch.RepoID > 0 && branch.Repo == nil
+	})
+
+	reposMap := make(map[int64]*repo_model.Repository, len(ids))
+	if err := db.GetEngine(ctx).In("id", ids).Find(&reposMap); err != nil {
+		return err
+	}
+	for _, branch := range branches {
+		if branch.RepoID <= 0 || branch.Repo != nil {
+			continue
+		}
+		branch.Repo = reposMap[branch.RepoID]
+	}
+	return nil
+}
+
 type FindBranchOptions struct {
 	db.ListOptions
 	RepoID             int64
diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go
index 7924517f31..cf7acdf83b 100644
--- a/models/organization/org_user_test.go
+++ b/models/organization/org_user_test.go
@@ -81,7 +81,7 @@ func TestUserListIsPublicMember(t *testing.T) {
 		{3, map[int64]bool{2: true, 4: false, 28: true}},
 		{6, map[int64]bool{5: true, 28: true}},
 		{7, map[int64]bool{5: false}},
-		{25, map[int64]bool{24: true}},
+		{25, map[int64]bool{12: true, 24: true}},
 		{22, map[int64]bool{}},
 	}
 	for _, v := range tt {
@@ -108,8 +108,8 @@ func TestUserListIsUserOrgOwner(t *testing.T) {
 		{3, map[int64]bool{2: true, 4: false, 28: false}},
 		{6, map[int64]bool{5: true, 28: false}},
 		{7, map[int64]bool{5: true}},
-		{25, map[int64]bool{24: false}}, // ErrTeamNotExist
-		{22, map[int64]bool{}},          // No member
+		{25, map[int64]bool{12: true, 24: false}}, // ErrTeamNotExist
+		{22, map[int64]bool{}},                    // No member
 	}
 	for _, v := range tt {
 		t.Run(fmt.Sprintf("IsUserOrgOwnerOfOrgId%d", v.orgid), func(t *testing.T) {
diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go
index 987c7df9b0..eacc98e222 100644
--- a/models/repo/repo_list.go
+++ b/models/repo/repo_list.go
@@ -175,6 +175,8 @@ type SearchRepoOptions struct {
 	// True -> include just forks
 	// False -> include just non-forks
 	Fork optional.Option[bool]
+	// If Fork option is True, you can use this option to limit the forks of a special repo by repo id.
+	ForkFrom int64
 	// None -> include templates AND non-templates
 	// True -> include just templates
 	// False -> include just non-templates
@@ -514,6 +516,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
 			cond = cond.And(builder.Eq{"is_fork": false})
 		} else {
 			cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()})
+
+			if opts.ForkFrom > 0 && opts.Fork.Value() {
+				cond = cond.And(builder.Eq{"fork_id": opts.ForkFrom})
+			}
 		}
 	}
 
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index e4e6201c24..e1498c0d58 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -29,6 +29,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	issue_model "code.gitea.io/gitea/models/issues"
+	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"
@@ -1027,15 +1028,26 @@ func renderHomeCode(ctx *context.Context) {
 			return
 		}
 
-		showRecentlyPushedNewBranches := true
-		if ctx.Repo.Repository.IsMirror ||
-			!ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
-			showRecentlyPushedNewBranches = false
+		opts := &git_model.FindRecentlyPushedNewBranchesOptions{
+			Repo:     ctx.Repo.Repository,
+			BaseRepo: ctx.Repo.Repository,
 		}
-		if showRecentlyPushedNewBranches {
-			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID, ctx.Repo.Repository.DefaultBranch)
+		if ctx.Repo.Repository.IsFork {
+			opts.BaseRepo = ctx.Repo.Repository.BaseRepo
+		}
+
+		baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
+		if err != nil {
+			ctx.ServerError("GetUserRepoPermission", err)
+			return
+		}
+
+		if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
+			opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
+			baseRepoPerm.CanRead(unit_model.TypePullRequests) {
+			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
 			if err != nil {
-				ctx.ServerError("GetRecentlyPushedBranches", err)
+				ctx.ServerError("FindRecentlyPushedNewBranches", err)
 				return
 			}
 		}
diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index b808f413d3..7f613fcba7 100644
--- a/templates/repo/code/recently_pushed_new_branches.tmpl
+++ b/templates/repo/code/recently_pushed_new_branches.tmpl
@@ -2,10 +2,10 @@
 	<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}}
+			{{$branchLink := HTMLFormat `<a href="%s">%s</a>` .BranchLink .BranchDisplayName}}
 			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}
 		</div>
-		<a role="button" class="ui compact green button tw-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}">
+		<a role="button" class="ui compact green button tw-m-0" href="{{.BranchCompareURL}}">
 			{{ctx.Locale.Tr "repo.pulls.compare_changes"}}
 		</a>
 	</div>
diff --git a/tests/integration/api_user_orgs_test.go b/tests/integration/api_user_orgs_test.go
index b6b4b6f2b2..c656ded5ae 100644
--- a/tests/integration/api_user_orgs_test.go
+++ b/tests/integration/api_user_orgs_test.go
@@ -29,6 +29,7 @@ func TestUserOrgs(t *testing.T) {
 
 	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
 	org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
+	org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "private_org35"})
 
 	assert.Equal(t, []*api.Organization{
 		{
@@ -55,6 +56,18 @@ func TestUserOrgs(t *testing.T) {
 			Location:    "",
 			Visibility:  "public",
 		},
+		{
+			ID:          35,
+			Name:        org35.Name,
+			UserName:    org35.Name,
+			FullName:    org35.FullName,
+			Email:       org35.Email,
+			AvatarURL:   org35.AvatarLink(db.DefaultContext),
+			Description: "",
+			Website:     "",
+			Location:    "",
+			Visibility:  "private",
+		},
 	}, orgs)
 
 	// user itself should get it's org's he is a member of
@@ -102,6 +115,7 @@ func TestMyOrgs(t *testing.T) {
 	DecodeJSON(t, resp, &orgs)
 	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
 	org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
+	org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "private_org35"})
 
 	assert.Equal(t, []*api.Organization{
 		{
@@ -128,5 +142,17 @@ func TestMyOrgs(t *testing.T) {
 			Location:    "",
 			Visibility:  "public",
 		},
+		{
+			ID:          35,
+			Name:        org35.Name,
+			UserName:    org35.Name,
+			FullName:    org35.FullName,
+			Email:       org35.Email,
+			AvatarURL:   org35.AvatarLink(db.DefaultContext),
+			Description: "",
+			Website:     "",
+			Location:    "",
+			Visibility:  "private",
+		},
 	}, orgs)
 }
diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go
index 7fb8dbc332..9f73ac80e2 100644
--- a/tests/integration/compare_test.go
+++ b/tests/integration/compare_test.go
@@ -140,7 +140,7 @@ func TestCompareCodeExpand(t *testing.T) {
 
 		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 		session = loginUser(t, user2.Name)
-		testRepoFork(t, session, user1.Name, repo.Name, user2.Name, "test_blob_excerpt-fork")
+		testRepoFork(t, session, user1.Name, repo.Name, user2.Name, "test_blob_excerpt-fork", "")
 		testCreateBranch(t, session, user2.Name, "test_blob_excerpt-fork", "branch/main", "forked-branch", http.StatusSeeOther)
 		testEditFile(t, session, user2.Name, "test_blob_excerpt-fork", "forked-branch", "README.md", strings.Repeat("a\n", 15)+"CHANGED\n"+strings.Repeat("a\n", 15))
 
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
index ea393a6061..002aa5600e 100644
--- a/tests/integration/empty_repo_test.go
+++ b/tests/integration/empty_repo_test.go
@@ -6,9 +6,11 @@ package integration
 import (
 	"bytes"
 	"encoding/base64"
+	"fmt"
 	"io"
 	"mime/multipart"
 	"net/http"
+	"net/http/httptest"
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
@@ -24,6 +26,17 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder {
+	url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch)
+	req := NewRequestWithValues(t, "POST", url, map[string]string{
+		"_csrf":         GetCSRF(t, session, "/user/settings"),
+		"commit_choice": "direct",
+		"tree_path":     treePath,
+		"content":       content,
+	})
+	return session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
 func TestEmptyRepo(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	subPaths := []string{
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index f9bd352b62..18f415083c 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -485,6 +485,7 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile
 	assert.True(t, result.Valid())
 }
 
+// GetCSRF returns CSRF token from body
 func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
 	t.Helper()
 	req := NewRequest(t, "GET", urlStr)
@@ -492,3 +493,11 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
 	doc := NewHTMLParser(t, resp.Body)
 	return doc.GetCSRF()
 }
+
+// GetCSRFFrom returns CSRF token from body
+func GetCSRFFromCookie(t testing.TB, session *TestSession, urlStr string) string {
+	t.Helper()
+	req := NewRequest(t, "GET", urlStr)
+	session.MakeRequest(t, req, http.StatusOK)
+	return session.GetCookie("_csrf").Value
+}
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
index 39d9103dfd..aed699fd20 100644
--- a/tests/integration/pull_compare_test.go
+++ b/tests/integration/pull_compare_test.go
@@ -45,7 +45,7 @@ func TestPullCompare(t *testing.T) {
 		defer tests.PrepareTestEnv(t)()
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
 		testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
 		testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go
index 7add8e1db6..5a06a7817f 100644
--- a/tests/integration/pull_create_test.go
+++ b/tests/integration/pull_create_test.go
@@ -85,7 +85,7 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b
 func TestPullCreate(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 
@@ -113,7 +113,7 @@ func TestPullCreate(t *testing.T) {
 func TestPullCreate_TitleEscape(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "<i>XSS PR</i>")
 
@@ -177,7 +177,7 @@ func TestPullBranchDelete(t *testing.T) {
 		defer tests.PrepareTestEnv(t)()
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
 		testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index 979c408388..3e7054c7e8 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -95,7 +95,7 @@ func TestPullMerge(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
@@ -117,7 +117,7 @@ func TestPullRebase(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
@@ -139,7 +139,7 @@ func TestPullRebaseMerge(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
@@ -161,7 +161,7 @@ func TestPullSquash(t *testing.T) {
 		hookTasksLenBefore := len(hookTasks)
 
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
 
@@ -180,7 +180,7 @@ func TestPullSquash(t *testing.T) {
 func TestPullCleanUpAfterMerge(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
@@ -215,7 +215,7 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
 func TestCantMergeWorkInProgress(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "[wip] This is a pull title")
@@ -234,7 +234,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
 func TestCantMergeConflict(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
 
@@ -280,7 +280,7 @@ func TestCantMergeConflict(t *testing.T) {
 func TestCantMergeUnrelated(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
 
 		// Now we want to create a commit on a branch that is totally unrelated to our current head
@@ -375,7 +375,7 @@ 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")
+		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
@@ -416,7 +416,7 @@ func TestFastForwardOnlyMerge(t *testing.T) {
 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")
+		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")
 
@@ -539,7 +539,7 @@ func TestPullRetargetChildOnBranchDelete(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
 		testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)")
 
 		respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request")
@@ -568,7 +568,7 @@ func TestPullRetargetChildOnBranchDelete(t *testing.T) {
 func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)")
 
@@ -599,7 +599,7 @@ func TestPullMergeIndexerNotifier(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
 		// create a pull request
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull")
 
@@ -676,7 +676,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
 		session := loginUser(t, "user1")
 		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 		forkedName := "repo1-1"
-		testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
+		testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
 		defer func() {
 			testDeleteRepository(t, session, "user1", forkedName)
 		}()
@@ -759,7 +759,7 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) {
 		session := loginUser(t, "user1")
 		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 		forkedName := "repo1-2"
-		testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
+		testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
 		defer func() {
 			testDeleteRepository(t, session, "user1", forkedName)
 		}()
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index df5d7b38ea..5ecf3ef469 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -186,7 +186,7 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
 		user2Session := loginUser(t, "user2")
 
 		// Have user1 create a fork of repo1.
-		testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1", "")
 
 		t.Run("Submit approve/reject review on merged PR", func(t *testing.T) {
 			// Create a merged PR (made by user1) in the upstream repo1.
diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go
index 80eea34513..26e1baeb11 100644
--- a/tests/integration/pull_status_test.go
+++ b/tests/integration/pull_status_test.go
@@ -23,7 +23,7 @@ import (
 func TestPullCreate_CommitStatus(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
 
 		url := path.Join("user1", "repo1", "compare", "master...status1")
@@ -122,7 +122,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
 	// so we need to have this meta commit also in develop branch.
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
 		testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1")
 
@@ -147,7 +147,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
 func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther)
 		url := path.Join("user1", "repo1", "compare", "master...status1")
 		req := NewRequestWithValues(t, "POST", url,
diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go
index 792554db4b..b04560379d 100644
--- a/tests/integration/repo_activity_test.go
+++ b/tests/integration/repo_activity_test.go
@@ -20,7 +20,7 @@ func TestRepoActivity(t *testing.T) {
 		session := loginUser(t, "user1")
 
 		// Create PRs (1 merged & 2 proposed)
-		testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 		testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
 		resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
 		elem := strings.Split(test.RedirectURL(resp), "/")
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index baa8da4b75..d1bc9198c3 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -4,26 +4,37 @@
 package integration
 
 import (
+	"fmt"
 	"net/http"
 	"net/url"
 	"path"
 	"strings"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
+	org_model "code.gitea.io/gitea/models/organization"
+	"code.gitea.io/gitea/models/perm"
+	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/setting"
+	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/PuerkitoBio/goquery"
 	"github.com/stretchr/testify/assert"
 )
 
 func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string {
 	var csrf string
 	if expectedStatus == http.StatusNotFound {
-		csrf = GetCSRF(t, session, path.Join(user, repo, "src/branch/master"))
+		// src/branch/branch_name may not container "_csrf" input,
+		// so we need to get it from cookies not from body
+		csrf = GetCSRFFromCookie(t, session, path.Join(user, repo, "src/branch/master"))
 	} else {
-		csrf = GetCSRF(t, session, path.Join(user, repo, "src", oldRefSubURL))
+		csrf = GetCSRFFromCookie(t, session, path.Join(user, repo, "src", oldRefSubURL))
 	}
 	req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefSubURL), map[string]string{
 		"_csrf":           csrf,
@@ -145,3 +156,136 @@ func TestCreateBranchInvalidCSRF(t *testing.T) {
 		strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()),
 	)
 }
+
+func prepareBranch(t *testing.T, session *TestSession, repo *repo_model.Repository) {
+	baseRefSubURL := fmt.Sprintf("branch/%s", repo.DefaultBranch)
+
+	// create branch with no new commit
+	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "no-commit", http.StatusSeeOther)
+
+	// create branch with commit
+	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "new-commit", http.StatusSeeOther)
+	testAPINewFile(t, session, repo.OwnerName, repo.Name, "new-commit", "new-commit.txt", "new-commit")
+
+	// create deleted branch
+	testCreateBranch(t, session, repo.OwnerName, repo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther)
+	testUIDeleteBranch(t, session, repo.OwnerName, repo.Name, "deleted-branch")
+}
+
+func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository, headBranch, title string) string {
+	srcRef := headBranch
+	if baseRepo.ID != headRepo.ID {
+		srcRef = fmt.Sprintf("%s/%s:%s", headRepo.OwnerName, headRepo.Name, headBranch)
+	}
+	resp := testPullCreate(t, session, baseRepo.OwnerName, baseRepo.Name, false, baseRepo.DefaultBranch, srcRef, title)
+	elem := strings.Split(test.RedirectURL(resp), "/")
+	// return pull request ID
+	return elem[4]
+}
+
+func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	// create opening PR
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "opening-pr", http.StatusSeeOther)
+	testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "opening-pr", "opening pr")
+
+	// create closed PR
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "closed-pr", http.StatusSeeOther)
+	prID := testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "closed-pr", "closed pr")
+	testIssueClose(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID)
+
+	// create closed PR with deleted branch
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "closed-pr-deleted", http.StatusSeeOther)
+	prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "closed-pr-deleted", "closed pr with deleted branch")
+	testIssueClose(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID)
+	testUIDeleteBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "closed-pr-deleted")
+
+	// create merged PR
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "merged-pr", http.StatusSeeOther)
+	prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "merged-pr", "merged pr")
+	testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "merged-pr", fmt.Sprintf("new-commit-%s.txt", headRepo.Name), "new-commit")
+	testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, repo_model.MergeStyleRebaseMerge, false)
+
+	// create merged PR with deleted branch
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "merged-pr-deleted", http.StatusSeeOther)
+	prID = testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "merged-pr-deleted", "merged pr with deleted branch")
+	testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "merged-pr-deleted", fmt.Sprintf("new-commit-%s-2.txt", headRepo.Name), "new-commit")
+	testPullMerge(t, baseSession, baseRepo.OwnerName, baseRepo.Name, prID, repo_model.MergeStyleRebaseMerge, true)
+}
+
+func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath string, expected []string) {
+	branches := make([]string, 0, 2)
+	req := NewRequest(t, "GET", repoPath)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	doc := NewHTMLParser(t, resp.Body)
+	doc.doc.Find(".ui.positive.message div a").Each(func(index int, branch *goquery.Selection) {
+		branches = append(branches, branch.Text())
+	})
+	assert.Equal(t, expected, branches)
+}
+
+func TestRecentlyPushedNewBranches(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		user1Session := loginUser(t, "user1")
+		user2Session := loginUser(t, "user2")
+		user12Session := loginUser(t, "user12")
+		user13Session := loginUser(t, "user13")
+
+		// prepare branch and PRs in original repo
+		repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
+		prepareBranch(t, user12Session, repo10)
+		prepareRepoPR(t, user12Session, user12Session, repo10, repo10)
+
+		// outdated new branch should not be displayed
+		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"new-commit"})
+
+		// create a fork repo in public org
+		testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", "new-commit")
+		orgPublicForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 25, Name: "org25_fork_repo10"})
+		prepareRepoPR(t, user12Session, user12Session, repo10, orgPublicForkRepo)
+
+		// user12 is the owner of the repo10 and the organization org25
+		// in repo10, user12 has opening/closed/merged pr and closed/merged pr with deleted branch
+		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"org25/org25_fork_repo10:new-commit", "new-commit"})
+
+		userForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
+		testCtx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository)
+		t.Run("AddUser13AsCollaborator", doAPIAddCollaborator(testCtx, "user13", perm.AccessModeWrite))
+		prepareBranch(t, user13Session, userForkRepo)
+		prepareRepoPR(t, user13Session, user13Session, repo10, userForkRepo)
+
+		// create branch with same name in different repo by user13
+		testCreateBranch(t, user13Session, repo10.OwnerName, repo10.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
+		testCreateBranch(t, user13Session, userForkRepo.OwnerName, userForkRepo.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
+		testCreatePullToDefaultBranch(t, user13Session, repo10, userForkRepo, "same-name-branch", "same name branch pr")
+
+		// user13 pushed 2 branches with the same name in repo10 and repo11
+		// and repo11's branch has a pr, but repo10's branch doesn't
+		// in this case, we should get repo10's branch but not repo11's branch
+		checkRecentlyPushedNewBranches(t, user13Session, "user12/repo10", []string{"same-name-branch", "user13/repo11:new-commit"})
+
+		// create a fork repo in private org
+		testRepoFork(t, user1Session, repo10.OwnerName, repo10.Name, "private_org35", "org35_fork_repo10", "new-commit")
+		orgPrivateForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 35, Name: "org35_fork_repo10"})
+		prepareRepoPR(t, user1Session, user1Session, repo10, orgPrivateForkRepo)
+
+		// user1 is the owner of private_org35 and no write permission to repo10
+		// so user1 can only see the branch in org35_fork_repo10
+		checkRecentlyPushedNewBranches(t, user1Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:new-commit"})
+
+		// user2 push a branch in private_org35
+		testCreateBranch(t, user2Session, orgPrivateForkRepo.OwnerName, orgPrivateForkRepo.Name, "branch/new-commit", "user-read-permission", http.StatusSeeOther)
+		// convert write permission to read permission for code unit
+		token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization)
+		req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d", 24), &api.EditTeamOption{
+			Name:     "team24",
+			UnitsMap: map[string]string{"repo.code": "read"},
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusOK)
+		teamUnit := unittest.AssertExistsAndLoadBean(t, &org_model.TeamUnit{TeamID: 24, Type: unit.TypeCode})
+		assert.Equal(t, perm.AccessModeRead, teamUnit.AccessMode)
+		// user2 can see the branch as it is created by user2
+		checkRecentlyPushedNewBranches(t, user2Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:user-read-permission"})
+	})
+}
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index ca5d61ecc2..feebebf062 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -16,7 +16,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName string) *httptest.ResponseRecorder {
+func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName, forkBranch string) *httptest.ResponseRecorder {
 	forkOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: forkOwnerName})
 
 	// Step0: check the existence of the to-fork repo
@@ -41,9 +41,10 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO
 	_, 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))
 	req = NewRequestWithValues(t, "POST", link, map[string]string{
-		"_csrf":     htmlDoc.GetCSRF(),
-		"uid":       fmt.Sprintf("%d", forkOwner.ID),
-		"repo_name": forkRepoName,
+		"_csrf":              htmlDoc.GetCSRF(),
+		"uid":                fmt.Sprintf("%d", forkOwner.ID),
+		"repo_name":          forkRepoName,
+		"fork_single_branch": forkBranch,
 	})
 	session.MakeRequest(t, req, http.StatusSeeOther)
 
@@ -57,13 +58,13 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO
 func TestRepoFork(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user1")
-	testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+	testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
 }
 
 func TestRepoForkToOrg(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user2")
-	testRepoFork(t, session, "user2", "repo1", "org3", "repo1")
+	testRepoFork(t, session, "user2", "repo1", "org3", "repo1", "")
 
 	// Check that no more forking is allowed as user2 owns repository
 	//  and org3 organization that owner user2 is also now has forked this repository

From 3066114c2481b5f3a5e4cda65fdd22e768359e94 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Wed, 22 May 2024 00:25:10 +0000
Subject: [PATCH 028/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_pt-PT.ini | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index f4c77e4981..ea4c2d26dc 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -807,7 +807,7 @@ add_new_key=Adicionar Chave SSH
 add_new_gpg_key=Adicionar chave GPG
 key_content_ssh_placeholder=Começa com 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com', ou 'sk-ssh-ed25519@openssh.com'
 key_content_gpg_placeholder=Começa com '-----BEGIN PGP PUBLIC KEY BLOCK-----'
-add_new_principal=Adicional Protagonista
+add_new_principal=Adicionar protagonista
 ssh_key_been_used=Esta chave SSH já tinha sido adicionada ao servidor.
 ssh_key_name_used=Já existe uma chave SSH com o mesmo nome na sua conta.
 ssh_principal_been_used=Este protagonista já tinha sido adicionado ao servidor.
@@ -3348,6 +3348,7 @@ mirror_sync_create=sincronizou a nova referência <a href="%[2]s">%[3]s</a> para
 mirror_sync_delete=sincronizou e eliminou a referência <code>%[2]s</code> em <a href="%[1]s">%[3]s</a> da réplica
 approve_pull_request=`aprovou <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`sugeriu modificações para <a href="%[1]s">%[3]s#%[2]s</a>`
+publish_release=`lançou <a href="%[2]s"> "%[4]s" </a> em <a href="%[1]s">%[3]s</a>`
 review_dismissed=`descartou a revisão de <b>%[4]s</b> para <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Motivo:
 create_branch=criou o ramo <a href="%[2]s">%[3]s</a> em <a href="%[1]s">%[4]s</a>
@@ -3414,6 +3415,7 @@ error.unit_not_allowed=Não tem permissão para aceder a esta parte do repositó
 title=Pacotes
 desc=Gerir pacotes do repositório.
 empty=Ainda não há pacotes.
+no_metadata=Sem metadados.
 empty.documentation=Para obter mais informação sobre o registo de pacotes, veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>.
 empty.repo=Carregou um pacote mas este não é apresentado aqui? Vá às <a href="%[1]s">configurações do pacote</a> e ligue-o a este repositório.
 registry.documentation=Para mais informação sobre o registo %s, veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>.

From de6f0488a67ad65bd2ac40356b08a78a365414cd Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 22 May 2024 02:47:18 +0200
Subject: [PATCH 029/131] Add nix flake for dev shell (#30967)

To try it you need **nix** installed `nix-daemon ` running and your user
has to be member of the **nix-users** group. Or use NixOS.

then by just:
```sh
nix develop -c $SHELL
```
a dedicated development environment with all needed packages will be
created.
---
 flake.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 flake.nix  | 37 +++++++++++++++++++++++++++++++++
 2 files changed, 98 insertions(+)
 create mode 100644 flake.lock
 create mode 100644 flake.nix

diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000000..0b2278f080
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1715534503,
+        "narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000000..c6e915e9db
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,37 @@
+{
+  inputs = {
+    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+    flake-utils.url = "github:numtide/flake-utils";
+  };
+  outputs =
+    { nixpkgs, flake-utils, ... }:
+    flake-utils.lib.eachDefaultSystem (
+      system:
+      let
+        pkgs = nixpkgs.legacyPackages.${system};
+      in
+      {
+        devShells.default = pkgs.mkShell {
+          buildInputs = with pkgs; [
+            # generic
+            git
+            git-lfs
+            gnumake
+            gnused
+            gnutar
+            gzip
+
+            # frontend
+            nodejs_20
+
+            # linting
+            python312
+            poetry
+
+            # backend
+            go_1_22
+          ];
+        };
+      }
+    );
+}

From 945dfed6a2646a5b3957ebcc8a5c08daf7a2c41b Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 22 May 2024 22:06:22 +0800
Subject: [PATCH 030/131] Update Actions documentation missing feature (#31034)

Fix
https://github.com/go-gitea/gitea/issues/25897#issuecomment-2117145391

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: yp05327 <576951401@qq.com>
---
 docs/content/usage/actions/comparison.en-us.md | 4 ++++
 docs/content/usage/actions/comparison.zh-cn.md | 4 ++++
 2 files changed, 8 insertions(+)

diff --git a/docs/content/usage/actions/comparison.en-us.md b/docs/content/usage/actions/comparison.en-us.md
index 1ea3afac5b..5b084e09c4 100644
--- a/docs/content/usage/actions/comparison.en-us.md
+++ b/docs/content/usage/actions/comparison.en-us.md
@@ -108,6 +108,10 @@ See [Creating an annotation for an error](https://docs.github.com/en/actions/usi
 
 It's ignored by Gitea Actions now.
 
+### Expressions
+
+For [expressions](https://docs.github.com/en/actions/learn-github-actions/expressions), only [`always()`](https://docs.github.com/en/actions/learn-github-actions/expressions#always) is supported.
+
 ## Missing UI features
 
 ### Pre and Post steps
diff --git a/docs/content/usage/actions/comparison.zh-cn.md b/docs/content/usage/actions/comparison.zh-cn.md
index 16b2181ba2..79450e8eab 100644
--- a/docs/content/usage/actions/comparison.zh-cn.md
+++ b/docs/content/usage/actions/comparison.zh-cn.md
@@ -108,6 +108,10 @@ Gitea Actions目前不支持此功能。
 
 Gitea Actions目前不支持此功能。
 
+### 表达式
+
+对于 [表达式](https://docs.github.com/en/actions/learn-github-actions/expressions), 当前仅 [`always()`](https://docs.github.com/en/actions/learn-github-actions/expressions#always) 被支持。
+
 ## 缺失的UI功能
 
 ### 预处理和后处理步骤

From c9eac519961ecd5d0e1d6ee856ab532e8c16c65d Mon Sep 17 00:00:00 2001
From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
Date: Wed, 22 May 2024 07:39:46 -0700
Subject: [PATCH 031/131] Sync up deleted branches & action assets related
 cleanup documentation (#31022)

Syncs up docs associated to actions and deleted branch cleanup i.e. in
custom/app.example.ini and the config cheat sheet.
---
 custom/conf/app.example.ini                           | 11 +++++++++++
 .../administration/config-cheat-sheet.en-us.md        | 10 +++++++++-
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 4df843b8ce..afbd20eb56 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2036,6 +2036,17 @@ LEVEL = Info
 ;;   or only create new users if UPDATE_EXISTING is set to false
 ;UPDATE_EXISTING = true
 
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Cleanup expired actions assets
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;[cron.cleanup_actions]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;ENABLED = true
+;RUN_AT_START = true
+;SCHEDULE = @midnight
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; Clean-up deleted branches
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 6c429bb652..9ac1f5eb10 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -975,12 +975,20 @@ Default templates for project boards:
 - `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts.
 - `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false.
 
-## Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`)
+#### Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`)
 
 - `ENABLED`: **true**: Enable cleanup expired actions assets job.
 - `RUN_AT_START`: **true**: Run job at start time (if ENABLED).
 - `SCHEDULE`: **@midnight** : Cron syntax for the job.
 
+#### Cron - Cleanup Deleted Branches (`cron.deleted_branches_cleanup`)
+
+- `ENABLED`: **true**: Enable deleted branches cleanup.
+- `RUN_AT_START`: **true**: Run job at start time (if ENABLED).
+- `NOTICE_ON_SUCCESS`: **false**: Set to true to log a success message.
+- `SCHEDULE`: **@midnight**: Cron syntax for scheduling deleted branches cleanup.
+- `OLDER_THAN`: **24h**: Branches deleted OLDER_THAN ago will be cleaned up.
+
 ### Extended cron tasks (not enabled by default)
 
 #### Cron - Garbage collect all repositories (`cron.git_gc_repos`)

From 90f4cf51a3b3ceec849970fffaaefbd0a2c1eaf1 Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Wed, 22 May 2024 19:34:52 -0400
Subject: [PATCH 032/131] align s3 files with docker naming (#31050)

docker images have `-nightly`, this will append the same to binaries
uploaded to s3.
---
 .github/workflows/release-nightly.yml | 2 +-
 Makefile                              | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index fbaa27102c..10fe94b296 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -47,7 +47,7 @@ jobs:
         run: |
           REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
           echo "Cleaned name is ${REF_NAME}"
-          echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
+          echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
       - name: configure aws
         uses: aws-actions/configure-aws-credentials@v4
         with:
diff --git a/Makefile b/Makefile
index e8006e4031..80efcbe46d 100644
--- a/Makefile
+++ b/Makefile
@@ -88,7 +88,7 @@ ifneq ($(GITHUB_REF_TYPE),branch)
 	GITEA_VERSION ?= $(VERSION)
 else
 	ifneq ($(GITHUB_REF_NAME),)
-		VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))
+		VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))-nightly
 	else
 		VERSION ?= main
 	endif

From 6d119aafd163d74117336a2d637f4b05c09081e1 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Thu, 23 May 2024 00:25:10 +0000
Subject: [PATCH 033/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_pt-PT.ini | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index ea4c2d26dc..ea0f96e4f8 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -798,9 +798,9 @@ manage_ssh_keys=Gerir chaves SSH
 manage_ssh_principals=Gerir Protagonistas de Certificados SSH
 manage_gpg_keys=Gerir chaves GPG
 add_key=Adicionar chave
-ssh_desc=Essas chaves públicas SSH estão associadas à sua conta. As chaves privadas correspondentes permitem acesso total aos seus repositórios.
+ssh_desc=Estas chaves públicas SSH estão associadas à sua conta. As chaves privadas correspondentes permitem acesso total aos seus repositórios.
 principal_desc=Estes protagonistas de certificados SSH estão associados à sua conta e permitem acesso total aos seus repositórios.
-gpg_desc=Essas chaves GPG públicas estão associadas à sua conta. Mantenha as suas chaves privadas seguras, uma vez que elas permitem a validação dos cometimentos.
+gpg_desc=Estas chaves GPG públicas estão associadas à sua conta. Mantenha as suas chaves privadas seguras, uma vez que elas permitem a validação dos cometimentos.
 ssh_helper=<strong>Precisa de ajuda?</strong> Dê uma vista de olhos no guia do GitHub para <a href="%s">criar as suas próprias chaves SSH</a> ou para resolver <a href="%s">problemas comuns</a> que pode encontrar ao usar o SSH.
 gpg_helper=<strong>Precisa de ajuda?</strong> Dê uma vista de olhos no guia do GitHub <a href="%s">sobre GPG</a>.
 add_new_key=Adicionar Chave SSH

From 7b93d6c8f786fe201201060c1785d19a3a1a3be2 Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Thu, 23 May 2024 08:18:25 -0400
Subject: [PATCH 034/131] Alpine 3.20 has been released (#31047)

---
 Dockerfile          | 4 ++--
 Dockerfile.rootless | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index b647c0cd59..21a8ce0d75 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.20 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
@@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
               /go/src/code.gitea.io/gitea/environment-to-ini
 RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 
-FROM docker.io/library/alpine:3.19
+FROM docker.io/library/alpine:3.20
 LABEL maintainer="maintainers@gitea.io"
 
 EXPOSE 22 3000
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index dd7da97278..b1d2368252 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.22-alpine3.19 AS build-env
+FROM docker.io/library/golang:1.22-alpine3.20 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY ${GOPROXY:-direct}
@@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
               /go/src/code.gitea.io/gitea/environment-to-ini
 RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 
-FROM docker.io/library/alpine:3.19
+FROM docker.io/library/alpine:3.20
 LABEL maintainer="maintainers@gitea.io"
 
 EXPOSE 2222 3000

From 7ab0988af140aa3e0204979765f75961f1dc9c11 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Thu, 23 May 2024 21:01:02 +0800
Subject: [PATCH 035/131] Support setting the `default` attribute of the issue
 template dropdown field (#31045)

Fix #31044

According to [GitHub issue template
documentation](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#attributes-for-dropdown),
the `default` attribute can be used to specify the preselected option
for a dropdown field.
---
 modules/issue/template/template.go        | 25 ++++++
 modules/issue/template/template_test.go   | 92 +++++++++++++++++++++++
 templates/repo/issue/fields/dropdown.tmpl |  2 +-
 3 files changed, 118 insertions(+), 1 deletion(-)

diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go
index 3be48b9edc..cf5fcf28e5 100644
--- a/modules/issue/template/template.go
+++ b/modules/issue/template/template.go
@@ -91,6 +91,9 @@ func validateYaml(template *api.IssueTemplate) error {
 			if err := validateOptions(field, idx); err != nil {
 				return err
 			}
+			if err := validateDropdownDefault(position, field.Attributes); err != nil {
+				return err
+			}
 		case api.IssueFormFieldTypeCheckboxes:
 			if err := validateStringItem(position, field.Attributes, false, "description"); err != nil {
 				return err
@@ -249,6 +252,28 @@ func validateBoolItem(position errorPosition, m map[string]any, names ...string)
 	return nil
 }
 
+func validateDropdownDefault(position errorPosition, attributes map[string]any) error {
+	v, ok := attributes["default"]
+	if !ok {
+		return nil
+	}
+	defaultValue, ok := v.(int)
+	if !ok {
+		return position.Errorf("'default' should be an int")
+	}
+
+	options, ok := attributes["options"].([]any)
+	if !ok {
+		// should not happen
+		return position.Errorf("'options' is required and should be a array")
+	}
+	if defaultValue < 0 || defaultValue >= len(options) {
+		return position.Errorf("the value of 'default' is out of range")
+	}
+
+	return nil
+}
+
 type errorPosition string
 
 func (p errorPosition) Errorf(format string, a ...any) error {
diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go
index e24b962d61..481058754d 100644
--- a/modules/issue/template/template_test.go
+++ b/modules/issue/template/template_test.go
@@ -355,6 +355,96 @@ body:
 `,
 			wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
 		},
+		{
+			name: "dropdown default is not an integer",
+			content: `
+name: "test"
+about: "this is about"
+body:
+  - type: dropdown
+    id: "1"
+    attributes:
+      label: Label of dropdown
+      description: Description of dropdown
+      multiple: true
+      options:
+        - Option 1 of dropdown
+        - Option 2 of dropdown
+        - Option 3 of dropdown
+      default: "def"
+    validations:
+      required: true
+`,
+			wantErr: "body[0](dropdown): 'default' should be an int",
+		},
+		{
+			name: "dropdown default is out of range",
+			content: `
+name: "test"
+about: "this is about"
+body:
+  - type: dropdown
+    id: "1"
+    attributes:
+      label: Label of dropdown
+      description: Description of dropdown
+      multiple: true
+      options:
+        - Option 1 of dropdown
+        - Option 2 of dropdown
+        - Option 3 of dropdown
+      default: 3
+    validations:
+      required: true
+`,
+			wantErr: "body[0](dropdown): the value of 'default' is out of range",
+		},
+		{
+			name: "dropdown without default is valid",
+			content: `
+name: "test"
+about: "this is about"
+body:
+  - type: dropdown
+    id: "1"
+    attributes:
+      label: Label of dropdown
+      description: Description of dropdown
+      multiple: true
+      options:
+        - Option 1 of dropdown
+        - Option 2 of dropdown
+        - Option 3 of dropdown
+    validations:
+      required: true
+`,
+			want: &api.IssueTemplate{
+				Name:  "test",
+				About: "this is about",
+				Fields: []*api.IssueFormField{
+					{
+						Type: "dropdown",
+						ID:   "1",
+						Attributes: map[string]any{
+							"label":       "Label of dropdown",
+							"description": "Description of dropdown",
+							"multiple":    true,
+							"options": []any{
+								"Option 1 of dropdown",
+								"Option 2 of dropdown",
+								"Option 3 of dropdown",
+							},
+						},
+						Validations: map[string]any{
+							"required": true,
+						},
+						Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
+					},
+				},
+				FileName: "test.yaml",
+			},
+			wantErr: "",
+		},
 		{
 			name: "valid",
 			content: `
@@ -399,6 +489,7 @@ body:
         - Option 1 of dropdown
         - Option 2 of dropdown
         - Option 3 of dropdown
+      default: 1
     validations:
       required: true
   - type: checkboxes
@@ -475,6 +566,7 @@ body:
 								"Option 2 of dropdown",
 								"Option 3 of dropdown",
 							},
+							"default": 1,
 						},
 						Validations: map[string]any{
 							"required": true,
diff --git a/templates/repo/issue/fields/dropdown.tmpl b/templates/repo/issue/fields/dropdown.tmpl
index f4fa79738c..26505f58a5 100644
--- a/templates/repo/issue/fields/dropdown.tmpl
+++ b/templates/repo/issue/fields/dropdown.tmpl
@@ -2,7 +2,7 @@
 	{{template "repo/issue/fields/header" .}}
 	{{/* FIXME: required validation */}}
 	<div class="ui fluid selection dropdown {{if .item.Attributes.multiple}}multiple clearable{{end}}">
-		<input type="hidden" name="form-field-{{.item.ID}}" value="0">
+		<input type="hidden" name="form-field-{{.item.ID}}" value="{{.item.Attributes.default}}">
 		{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 		{{if not .item.Validations.required}}
 		{{svg "octicon-x" 14 "remove icon"}}

From ec771fdfcdbc74320b1ef0252444aa5cddd50a04 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Fri, 24 May 2024 00:25:44 +0000
Subject: [PATCH 036/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_pt-PT.ini | 2 +-
 options/locale/locale_zh-CN.ini | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index ea0f96e4f8..15635b4beb 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1595,7 +1595,7 @@ issues.label_title=Nome do rótulo
 issues.label_description=Descrição do rótulo
 issues.label_color=Cor do rótulo
 issues.label_exclusive=Exclusivo
-issues.label_archive=Rótulo de arquivo
+issues.label_archive=Arquivar rótulo
 issues.label_archived_filter=Mostrar rótulos arquivados
 issues.label_archive_tooltip=Os rótulos arquivados são, por norma, excluídos das sugestões ao pesquisar por rótulo.
 issues.label_exclusive_desc=Nomeie o rótulo <code>âmbito/item</code> para torná-lo mutuamente exclusivo com outros rótulos do <code>âmbito/</code>.
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 0e224f0061..75facb4dcb 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -3415,6 +3415,7 @@ error.unit_not_allowed=您没有权限访问此仓库单元
 title=软件包
 desc=管理仓库软件包。
 empty=还没有软件包。
+no_metadata=没有元数据。
 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>。

From 47e715a70ff1802fae27d8d922b3185a3d83d640 Mon Sep 17 00:00:00 2001
From: metiftikci <metiftikci@hotmail.com>
Date: Sat, 25 May 2024 17:02:07 +0300
Subject: [PATCH 037/131] Fix `View File` button link if branch deleted on pull
 request files pages (#31063)

as title
---
 routers/web/repo/pull.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index bbdc6ca631..92e0a1674e 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -862,7 +862,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 		}
 
 		if pull.HeadRepo != nil {
-			ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch)
+			ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/commit/" + endCommitID
 
 			if !pull.HasMerged && ctx.Doer != nil {
 				perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)

From 2ced31e81dd9e45659660c1abff529d0192fd8ed Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Sat, 25 May 2024 16:33:34 +0200
Subject: [PATCH 038/131] Change `--border-radius-circle` to
 `--border-radius-full` (#30936)

Percentage-based `border-radius` [creates undesirable
ellipse](https://jsfiddle.net/silverwind/j9ko5wnt/4/) on non-square
content. Instead, use pixel value and use same wording `full` like
tailwind does, but increast to 99999px over their 9999px.
---
 tailwind.config.js                 | 2 +-
 web_src/css/base.css               | 4 ++--
 web_src/css/modules/animations.css | 2 +-
 web_src/css/repo.css               | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/tailwind.config.js b/tailwind.config.js
index d49e9d7a1c..94dfdbced4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -66,7 +66,7 @@ export default {
       'xl': '12px',
       '2xl': '16px',
       '3xl': '24px',
-      'full': 'var(--border-radius-circle)', // 50%
+      'full': 'var(--border-radius-full)',
     },
     fontFamily: {
       sans: 'var(--fonts-regular)',
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 2d93690170..0e54d17262 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -18,7 +18,7 @@
   /* other variables */
   --border-radius: 4px;
   --border-radius-medium: 6px;
-  --border-radius-circle: 50%;
+  --border-radius-full: 99999px; /* TODO: use calc(infinity * 1px) */
   --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 */
@@ -1166,7 +1166,7 @@ overflow-menu .ui.label {
 
 .color-icon {
   display: inline-block;
-  border-radius: var(--border-radius-circle);
+  border-radius: var(--border-radius-full);
   height: 14px;
   width: 14px;
 }
diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index 361618c449..a86c9234aa 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -31,7 +31,7 @@
   border-width: 4px;
   border-style: solid;
   border-color: var(--color-secondary) var(--color-secondary) var(--color-secondary-dark-8) var(--color-secondary-dark-8);
-  border-radius: var(--border-radius-circle);
+  border-radius: var(--border-radius-full);
 }
 
 .is-loading.loading-icon-2px::after {
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 56235f8ebe..ce5d3c7951 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -790,7 +790,7 @@ td .commit-summary {
   width: 34px;
   height: 34px;
   background-color: var(--color-timeline);
-  border-radius: var(--border-radius-circle);
+  border-radius: var(--border-radius-full);
   display: flex;
   float: left;
   margin-left: -33px;

From 14f6105ce0c5802518b46d0af337b4e5f1af4f87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Rosenhammer?= <andre.rosenhammer@gmail.com>
Date: Sun, 26 May 2024 06:08:13 +0200
Subject: [PATCH 039/131] Make gitea webhooks openproject compatible (#28435)

This PR adds some fields to the gitea webhook payload that
[openproject](https://www.openproject.org/) expects to exists in order
to process the webhooks.
These fields do exists in Github's webhook payload so adding them makes
Gitea's native webhook more compatible towards Github's.
---
 models/issues/pull.go          | 15 ++++++++
 modules/structs/issue.go       |  1 +
 modules/structs/pull.go        |  6 ++++
 modules/structs/user.go        |  2 ++
 services/convert/issue.go      |  2 ++
 services/convert/pull.go       | 64 ++++++++++++++++++++++------------
 services/convert/user.go       |  1 +
 templates/swagger/v1_json.tmpl | 34 ++++++++++++++++++
 8 files changed, 102 insertions(+), 23 deletions(-)

diff --git a/models/issues/pull.go b/models/issues/pull.go
index 4194df2e3d..014fcd9fd0 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -430,6 +430,21 @@ func (pr *PullRequest) GetGitHeadBranchRefName() string {
 	return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch)
 }
 
+// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
+func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
+	opts := FindCommentsOptions{
+		Type:    CommentTypeReview,
+		IssueID: pr.IssueID,
+	}
+	conds := opts.ToConds()
+
+	count, err := db.GetEngine(ctx).Where(conds).Count(new(Comment))
+	if err != nil {
+		return 0
+	}
+	return int(count)
+}
+
 // IsChecking returns true if this pull request is still checking conflict.
 func (pr *PullRequest) IsChecking() bool {
 	return pr.Status == PullRequestStatusChecking
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 16242d18ad..3c06e38356 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -30,6 +30,7 @@ type PullRequestMeta struct {
 	HasMerged        bool       `json:"merged"`
 	Merged           *time.Time `json:"merged_at"`
 	IsWorkInProgress bool       `json:"draft"`
+	HTMLURL          string     `json:"html_url"`
 }
 
 // RepositoryMeta basic repository information
diff --git a/modules/structs/pull.go b/modules/structs/pull.go
index b04def52b8..525d90c28e 100644
--- a/modules/structs/pull.go
+++ b/modules/structs/pull.go
@@ -21,8 +21,14 @@ type PullRequest struct {
 	Assignees          []*User    `json:"assignees"`
 	RequestedReviewers []*User    `json:"requested_reviewers"`
 	State              StateType  `json:"state"`
+	Draft              bool       `json:"draft"`
 	IsLocked           bool       `json:"is_locked"`
 	Comments           int        `json:"comments"`
+	// number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
+	ReviewComments int `json:"review_comments"`
+	Additions      int `json:"additions"`
+	Deletions      int `json:"deletions"`
+	ChangedFiles   int `json:"changed_files"`
 
 	HTMLURL  string `json:"html_url"`
 	DiffURL  string `json:"diff_url"`
diff --git a/modules/structs/user.go b/modules/structs/user.go
index ca6ab79944..5ed677f239 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -28,6 +28,8 @@ type User struct {
 	Email string `json:"email"`
 	// URL to the user's avatar
 	AvatarURL string `json:"avatar_url"`
+	// URL to the user's gitea page
+	HTMLURL string `json:"html_url"`
 	// User locale
 	Language string `json:"language"`
 	// Is the user an administrator
diff --git a/services/convert/issue.go b/services/convert/issue.go
index 668affe09a..4fe7ef44fe 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -104,6 +104,8 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
 			if issue.PullRequest.HasMerged {
 				apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
 			}
+			// Add pr's html url
+			apiIssue.PullRequest.HTMLURL = issue.HTMLURL()
 		}
 	}
 	if issue.DeadlineUnix != 0 {
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 775bf3806d..6d95804b38 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -51,29 +51,31 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 	}
 
 	apiPullRequest := &api.PullRequest{
-		ID:        pr.ID,
-		URL:       pr.Issue.HTMLURL(),
-		Index:     pr.Index,
-		Poster:    apiIssue.Poster,
-		Title:     apiIssue.Title,
-		Body:      apiIssue.Body,
-		Labels:    apiIssue.Labels,
-		Milestone: apiIssue.Milestone,
-		Assignee:  apiIssue.Assignee,
-		Assignees: apiIssue.Assignees,
-		State:     apiIssue.State,
-		IsLocked:  apiIssue.IsLocked,
-		Comments:  apiIssue.Comments,
-		HTMLURL:   pr.Issue.HTMLURL(),
-		DiffURL:   pr.Issue.DiffURL(),
-		PatchURL:  pr.Issue.PatchURL(),
-		HasMerged: pr.HasMerged,
-		MergeBase: pr.MergeBase,
-		Mergeable: pr.Mergeable(ctx),
-		Deadline:  apiIssue.Deadline,
-		Created:   pr.Issue.CreatedUnix.AsTimePtr(),
-		Updated:   pr.Issue.UpdatedUnix.AsTimePtr(),
-		PinOrder:  apiIssue.PinOrder,
+		ID:             pr.ID,
+		URL:            pr.Issue.HTMLURL(),
+		Index:          pr.Index,
+		Poster:         apiIssue.Poster,
+		Title:          apiIssue.Title,
+		Body:           apiIssue.Body,
+		Labels:         apiIssue.Labels,
+		Milestone:      apiIssue.Milestone,
+		Assignee:       apiIssue.Assignee,
+		Assignees:      apiIssue.Assignees,
+		State:          apiIssue.State,
+		Draft:          pr.IsWorkInProgress(ctx),
+		IsLocked:       apiIssue.IsLocked,
+		Comments:       apiIssue.Comments,
+		ReviewComments: pr.GetReviewCommentsCount(ctx),
+		HTMLURL:        pr.Issue.HTMLURL(),
+		DiffURL:        pr.Issue.DiffURL(),
+		PatchURL:       pr.Issue.PatchURL(),
+		HasMerged:      pr.HasMerged,
+		MergeBase:      pr.MergeBase,
+		Mergeable:      pr.Mergeable(ctx),
+		Deadline:       apiIssue.Deadline,
+		Created:        pr.Issue.CreatedUnix.AsTimePtr(),
+		Updated:        pr.Issue.UpdatedUnix.AsTimePtr(),
+		PinOrder:       apiIssue.PinOrder,
 
 		AllowMaintainerEdit: pr.AllowMaintainerEdit,
 
@@ -168,6 +170,12 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 			return nil
 		}
 
+		// Outer scope variables to be used in diff calculation
+		var (
+			startCommitID string
+			endCommitID   string
+		)
+
 		if git.IsErrBranchNotExist(err) {
 			headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref)
 			if err != nil && !git.IsErrNotExist(err) {
@@ -176,6 +184,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 			}
 			if err == nil {
 				apiPullRequest.Head.Sha = headCommitID
+				endCommitID = headCommitID
 			}
 		} else {
 			commit, err := headBranch.GetCommit()
@@ -186,8 +195,17 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 			if err == nil {
 				apiPullRequest.Head.Ref = pr.HeadBranch
 				apiPullRequest.Head.Sha = commit.ID.String()
+				endCommitID = commit.ID.String()
 			}
 		}
+
+		// Calculate diff
+		startCommitID = pr.MergeBase
+
+		apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID)
+		if err != nil {
+			log.Error("GetDiffShortStat: %v", err)
+		}
 	}
 
 	if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
diff --git a/services/convert/user.go b/services/convert/user.go
index 2957c58b14..90bcf35cf6 100644
--- a/services/convert/user.go
+++ b/services/convert/user.go
@@ -53,6 +53,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
 		FullName:    user.FullName,
 		Email:       user.GetPlaceholderEmail(),
 		AvatarURL:   user.AvatarLink(ctx),
+		HTMLURL:     user.HTMLURL(),
 		Created:     user.CreatedUnix.AsTime(),
 		Restricted:  user.IsRestricted,
 		Location:    user.Location,
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 0b3f5cdcad..34829a15fc 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -22975,6 +22975,11 @@
       "description": "PullRequest represents a pull request",
       "type": "object",
       "properties": {
+        "additions": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "Additions"
+        },
         "allow_maintainer_edit": {
           "type": "boolean",
           "x-go-name": "AllowMaintainerEdit"
@@ -22996,6 +23001,11 @@
           "type": "string",
           "x-go-name": "Body"
         },
+        "changed_files": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "ChangedFiles"
+        },
         "closed_at": {
           "type": "string",
           "format": "date-time",
@@ -23011,10 +23021,19 @@
           "format": "date-time",
           "x-go-name": "Created"
         },
+        "deletions": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "Deletions"
+        },
         "diff_url": {
           "type": "string",
           "x-go-name": "DiffURL"
         },
+        "draft": {
+          "type": "boolean",
+          "x-go-name": "Draft"
+        },
         "due_date": {
           "type": "string",
           "format": "date-time",
@@ -23091,6 +23110,12 @@
           },
           "x-go-name": "RequestedReviewers"
         },
+        "review_comments": {
+          "description": "number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)",
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "ReviewComments"
+        },
         "state": {
           "$ref": "#/definitions/StateType"
         },
@@ -23121,6 +23146,10 @@
           "type": "boolean",
           "x-go-name": "IsWorkInProgress"
         },
+        "html_url": {
+          "type": "string",
+          "x-go-name": "HTMLURL"
+        },
         "merged": {
           "type": "boolean",
           "x-go-name": "HasMerged"
@@ -24414,6 +24443,11 @@
           "type": "string",
           "x-go-name": "FullName"
         },
+        "html_url": {
+          "description": "URL to the user's gitea page",
+          "type": "string",
+          "x-go-name": "HTMLURL"
+        },
         "id": {
           "description": "the user's id",
           "type": "integer",

From e625813aa9f585718e9c7677fc441f1f3ad69c61 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 27 May 2024 00:26:27 +0000
Subject: [PATCH 040/131] [skip ci] Updated licenses and gitignores

---
 ...e-first-lines => BSD-2-Clause-first-lines} |  0
 options/license/Gutmann                       |  2 +
 options/license/HPND-export2-US               | 21 ++++++
 options/license/HPND-merchantability-variant  |  9 +++
 options/license/RRDtool-FLOSS-exception-2.0   | 66 +++++++++++++++++++
 5 files changed, 98 insertions(+)
 rename options/license/{BSD-2-clause-first-lines => BSD-2-Clause-first-lines} (100%)
 create mode 100644 options/license/Gutmann
 create mode 100644 options/license/HPND-export2-US
 create mode 100644 options/license/HPND-merchantability-variant
 create mode 100644 options/license/RRDtool-FLOSS-exception-2.0

diff --git a/options/license/BSD-2-clause-first-lines b/options/license/BSD-2-Clause-first-lines
similarity index 100%
rename from options/license/BSD-2-clause-first-lines
rename to options/license/BSD-2-Clause-first-lines
diff --git a/options/license/Gutmann b/options/license/Gutmann
new file mode 100644
index 0000000000..c33f4ee3a2
--- /dev/null
+++ b/options/license/Gutmann
@@ -0,0 +1,2 @@
+You can use this code in whatever way you want, as long as you don't try
+to claim you wrote it.
diff --git a/options/license/HPND-export2-US b/options/license/HPND-export2-US
new file mode 100644
index 0000000000..1dda23a88c
--- /dev/null
+++ b/options/license/HPND-export2-US
@@ -0,0 +1,21 @@
+Copyright 2004-2008 Apple Inc.  All Rights Reserved.
+
+   Export of this software from the United States of America may
+   require a specific license from the United States Government.
+   It is the responsibility of any person or organization
+   contemplating export to obtain such a license before exporting.
+
+WITHIN THAT CONSTRAINT, 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 appear in all copies and that both that copyright notice and
+this permission notice appear in supporting documentation, and that
+the name of Apple Inc. not be used in advertising or publicity
+pertaining to distribution of the software without specific,
+written prior permission.  Apple Inc. makes no representations
+about the suitability of this software for any purpose.  It is
+provided "as is" without express or implied warranty.
+
+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/HPND-merchantability-variant b/options/license/HPND-merchantability-variant
new file mode 100644
index 0000000000..421b9ff96b
--- /dev/null
+++ b/options/license/HPND-merchantability-variant
@@ -0,0 +1,9 @@
+Copyright (C) 2004 Christian Groessler <chris@groessler.org>
+
+Permission to use, copy, modify, and distribute this file
+for any purpose is hereby granted without fee, provided that
+the above copyright notice and this notice appears in all
+copies.
+
+This file is distributed WITHOUT ANY WARRANTY; without even the implied
+warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/options/license/RRDtool-FLOSS-exception-2.0 b/options/license/RRDtool-FLOSS-exception-2.0
new file mode 100644
index 0000000000..d88dae5868
--- /dev/null
+++ b/options/license/RRDtool-FLOSS-exception-2.0
@@ -0,0 +1,66 @@
+FLOSS License Exception 
+=======================
+(Adapted from http://www.mysql.com/company/legal/licensing/foss-exception.html)
+
+I want specified Free/Libre and Open Source Software ("FLOSS")
+applications to be able to use specified GPL-licensed RRDtool
+libraries (the "Program") despite the fact that not all FLOSS licenses are
+compatible with version 2 of the GNU General Public License (the "GPL").
+
+As a special exception to the terms and conditions of version 2.0 of the GPL:
+
+You are free to distribute a Derivative Work that is formed entirely from
+the Program and one or more works (each, a "FLOSS Work") licensed under one
+or more of the licenses listed below, as long as:
+
+1. You obey the GPL in all respects for the Program and the Derivative
+Work, except for identifiable sections of the Derivative Work which are
+not derived from the Program, and which can reasonably be considered
+independent and separate works in themselves,
+
+2. all identifiable sections of the Derivative Work which are not derived
+from the Program, and which can reasonably be considered independent and
+separate works in themselves,
+
+1. are distributed subject to one of the FLOSS licenses listed
+below, and
+
+2. the object code or executable form of those sections are
+accompanied by the complete corresponding machine-readable source
+code for those sections on the same medium and under the same FLOSS
+license as the corresponding object code or executable forms of
+those sections, and
+
+3. any works which are aggregated with the Program or with a Derivative
+Work on a volume of a storage or distribution medium in accordance with
+the GPL, can reasonably be considered independent and separate works in
+themselves which are not derivatives of either the Program, a Derivative
+Work or a FLOSS Work.
+
+If the above conditions are not met, then the Program may only be copied,
+modified, distributed or used under the terms and conditions of the GPL.
+
+FLOSS License List
+==================
+License name	Version(s)/Copyright Date
+Academic Free License		2.0
+Apache Software License	1.0/1.1/2.0
+Apple Public Source License	2.0
+Artistic license		From Perl 5.8.0
+BSD license			"July 22 1999"
+Common Public License		1.0
+GNU Library or "Lesser" General Public License (LGPL)	2.0/2.1
+IBM Public License, Version    1.0
+Jabber Open Source License	1.0
+MIT License (As listed in file MIT-License.txt)	-
+Mozilla Public License (MPL)	1.0/1.1
+Open Software License		2.0
+OpenSSL license (with original SSLeay license)	"2003" ("1998")
+PHP License			3.01
+Python license (CNRI Python License)	-
+Python Software Foundation License	2.1.1
+Sleepycat License		"1999"
+W3C License			"2001"
+X11 License			"2001"
+Zlib/libpng License		-
+Zope Public License		2.0/2.1

From 145baa2b3f3bef2b4535d6d3b7b2cdb88da4382b Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 27 May 2024 06:48:41 +0200
Subject: [PATCH 041/131] Fix border radius on hovered secondary menu (#31089)

Presumably a regression from
https://github.com/go-gitea/gitea/pull/30325, these menus were showing a
border radius on hover, which is fixed with this change.

<img width="154" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/eafdc1c5-3cf5-48d1-86c4-21c58f92cfaf">
---
 web_src/css/modules/menu.css | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/web_src/css/modules/menu.css b/web_src/css/modules/menu.css
index ff9d7fc5d0..43679a3317 100644
--- a/web_src/css/modules/menu.css
+++ b/web_src/css/modules/menu.css
@@ -512,11 +512,14 @@
   background: var(--color-hover);
 }
 
+.ui.secondary.menu .active.item {
+  border-radius: 0.28571429rem;
+}
+
 .ui.secondary.menu .active.item,
 .ui.secondary.menu .active.item:hover {
   color: var(--color-text-dark);
   background: var(--color-active);
-  border-radius: 0.28571429rem;
 }
 
 .ui.secondary.item.menu {

From e695ba47557ed4c3999c63b28051a449ca4653de Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 27 May 2024 13:21:00 +0800
Subject: [PATCH 042/131] Fix possible ui 500 if workflow's job is nil (#31092)

Fix #31087
---
 options/locale/locale_en-US.ini     | 1 +
 routers/web/repo/actions/actions.go | 8 ++++++++
 2 files changed, 9 insertions(+)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index db4e3ec56b..40cbdb23fe 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3638,6 +3638,7 @@ 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.no_job = The workflow must contain at least one job
 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 6059ad1414..a0f03ec7e9 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -107,7 +107,12 @@ func List(ctx *context.Context) {
 			// 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"
+			emptyJobsNumber := 0
 			for _, j := range wf.Jobs {
+				if j == nil {
+					emptyJobsNumber++
+					continue
+				}
 				if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
 					hasJobWithoutNeeds = true
 				}
@@ -131,6 +136,9 @@ func List(ctx *context.Context) {
 			if !hasJobWithoutNeeds {
 				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
 			}
+			if emptyJobsNumber == len(wf.Jobs) {
+				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
+			}
 			workflows = append(workflows, workflow)
 		}
 	}

From 31a0c4dfb4156a7b4d856cceae1e61c7fc1a4a1b Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Mon, 27 May 2024 14:15:34 +0800
Subject: [PATCH 043/131] Improve the handling of `jobs.<job_id>.if` (#31070)

Fix #25897
Fix #30322

#29464 cannot handle some complex `if` conditions correctly because it
only checks `always()` literally. In fact, it's not easy to evaluate the
`if` condition on the Gitea side because evaluating it requires a series
of contexts. But act_runner is able to evaluate the `if` condition
before running the job (for more information, see
[`gitea/act`](https://gitea.com/gitea/act/src/commit/517d11c67126bd97c88e2faabda0832fff482258/pkg/runner/run_context.go#L739-L753))
. So we can use act_runner to check the `if` condition.

In this PR, how to handle a blocked job depends on its `needs` and `if`:
- If not all jobs in `needs` completed successfully and the job's `if`
is empty, set the job status to `StatusSkipped`
- In other cases, the job status will be set to `StatusWaiting`, and
then act_runner will check the `if` condition and run the job if the
condition is met
---
 services/actions/job_emitter.go      | 14 +++++++-------
 services/actions/job_emitter_test.go | 18 +++++++++---------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index d2bbbd9a7c..1f859fcf70 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -7,7 +7,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
@@ -141,18 +140,19 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
 			if allSucceed {
 				ret[id] = actions_model.StatusWaiting
 			} else {
-				// 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
+				// Check if the job has an "if" condition
+				hasIf := 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()"
+					hasIf = len(wfJob.If.Value) > 0
 				}
 
-				if always {
+				if hasIf {
+					// act_runner will check the "if" condition
 					ret[id] = actions_model.StatusWaiting
 				} else {
+					// If the "if" condition is empty and not all dependent jobs completed successfully,
+					// the job should be skipped.
 					ret[id] = actions_model.StatusSkipped
 				}
 			}
diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go
index 038df7d4f8..58c2dc3b24 100644
--- a/services/actions/job_emitter_test.go
+++ b/services/actions/job_emitter_test.go
@@ -71,9 +71,9 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
 			want: map[int64]actions_model.Status{},
 		},
 		{
-			name: "with ${{ always() }} condition",
+			name: "`if` is not empty and all jobs in `needs` completed successfully",
 			jobs: actions_model.ActionJobList{
-				{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+				{ID: 1, JobID: "job1", Status: actions_model.StatusSuccess, Needs: []string{}},
 				{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
 					`
 name: test
@@ -82,15 +82,15 @@ jobs:
   job2:
     runs-on: ubuntu-latest
     needs: job1
-    if: ${{ always() }}
+    if: ${{ always() && needs.job1.result == 'success' }}
     steps:
-      - run: echo "always run"
+      - run: echo "will be checked by act_runner"
 `)},
 			},
 			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
 		},
 		{
-			name: "with always() condition",
+			name: "`if` is not empty and not all jobs in `needs` completed successfully",
 			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(
@@ -101,15 +101,15 @@ jobs:
   job2:
     runs-on: ubuntu-latest
     needs: job1
-    if: always()
+    if: ${{ always() && needs.job1.result == 'failure' }}
     steps:
-      - run: echo "always run"
+      - run: echo "will be checked by act_runner"
 `)},
 			},
 			want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
 		},
 		{
-			name: "without always() condition",
+			name: "`if` is empty and not all jobs in `needs` completed successfully",
 			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(
@@ -121,7 +121,7 @@ jobs:
     runs-on: ubuntu-latest
     needs: job1
     steps:
-      - run: echo "not always run"
+      - run: echo "should be skipped"
 `)},
 			},
 			want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},

From 6e140b58ddd318f8e916b1f83551c6b2c8291510 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 27 May 2024 08:45:16 +0200
Subject: [PATCH 044/131] Prevent tab shifting, remove extra margin on fluid
 pages (#31090)

1. Extend concept of https://github.com/go-gitea/gitea/pull/29831 to all
tabular menus, there were only three left that weren't already
`<overflow-menu>`.

<img width="634" alt="Screenshot 2024-05-27 at 00 42 16"
src="https://github.com/go-gitea/gitea/assets/115237/d9a7e219-d05e-40a1-9e93-777f9a8a90dd">
<img width="965" alt="Screenshot 2024-05-27 at 00 29 32"
src="https://github.com/go-gitea/gitea/assets/115237/e6ed71b1-11fb-4a74-9adb-af4524286cff">

2. Remove extra padding on `fluid padded` container like for example PR
diff view. The page margin is already correctly sized via
`.ui.container`, so this was just extraneous padding that looked ugly.

Before:
<img width="1351" alt="Screenshot 2024-05-27 at 00 45 11"
src="https://github.com/go-gitea/gitea/assets/115237/4b45fd11-b1b2-4fbb-a618-26eb22be9472">

After:
<img width="1344" alt="Screenshot 2024-05-27 at 00 45 22"
src="https://github.com/go-gitea/gitea/assets/115237/d09593eb-6c7f-45e7-85b6-f0050047004b">

3. Replace `gt-word-break` with `tw-break-anywhere` in issue-title,
fixing overflow.

Before:
<img width="1333" alt="Screenshot 2024-05-27 at 00 50 14"
src="https://github.com/go-gitea/gitea/assets/115237/64d15d04-b456-401e-a972-df636965f0eb">

After:
<img width="1316" alt="Screenshot 2024-05-27 at 00 50 26"
src="https://github.com/go-gitea/gitea/assets/115237/ed1ce830-1408-414b-8263-eeaf773f52c8">
---
 templates/repo/issue/view_title.tmpl         |  2 +-
 templates/repo/pulls/tab_menu.tmpl           |  6 +++---
 templates/repo/settings/webhook/history.tmpl | 10 ++++++----
 templates/shared/combomarkdowneditor.tmpl    |  4 ++--
 templates/shared/misc/tabtitle.tmpl          |  1 +
 web_src/css/modules/container.css            |  4 ----
 6 files changed, 13 insertions(+), 14 deletions(-)
 create mode 100644 templates/shared/misc/tabtitle.tmpl

diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 097d7b1f7c..58d3759a9d 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -6,7 +6,7 @@
 <div class="issue-title-header">
 	{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
 	<div class="issue-title" id="issue-title-display">
-		<h1 class="gt-word-break">
+		<h1 class="tw-break-anywhere">
 			{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}}
 			<span class="index">#{{.Issue.Index}}</span>
 		</h1>
diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl
index d5a8d6ed21..8b192c44db 100644
--- a/templates/repo/pulls/tab_menu.tmpl
+++ b/templates/repo/pulls/tab_menu.tmpl
@@ -2,17 +2,17 @@
 	<div class="ui top attached pull tabular menu">
 		<a class="item {{if .PageIsPullConversation}}active{{end}}" href="{{.Issue.Link}}">
 			{{svg "octicon-comment-discussion"}}
-			{{ctx.Locale.Tr "repo.pulls.tab_conversation"}}
+			{{template "shared/misc/tabtitle" (ctx.Locale.Tr "repo.pulls.tab_conversation")}}
 			<span class="ui small label">{{.Issue.NumComments}}</span>
 		</a>
 		<a class="item {{if .PageIsPullCommits}}active{{end}}" {{if .NumCommits}}href="{{.Issue.Link}}/commits"{{end}}>
 			{{svg "octicon-git-commit"}}
-			{{ctx.Locale.Tr "repo.pulls.tab_commits"}}
+			{{template "shared/misc/tabtitle" (ctx.Locale.Tr "repo.pulls.tab_commits")}}
 			<span class="ui small label">{{if .NumCommits}}{{.NumCommits}}{{else}}-{{end}}</span>
 		</a>
 		<a class="item {{if .PageIsPullFiles}}active{{end}}" href="{{.Issue.Link}}/files">
 			{{svg "octicon-diff"}}
-			{{ctx.Locale.Tr "repo.pulls.tab_files"}}
+			{{template "shared/misc/tabtitle" (ctx.Locale.Tr "repo.pulls.tab_files")}}
 			<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
 		</a>
 		{{if or .Diff.TotalAddition .Diff.TotalDeletion}}
diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl
index 149840b0de..0e03b8ed1b 100644
--- a/templates/repo/settings/webhook/history.tmpl
+++ b/templates/repo/settings/webhook/history.tmpl
@@ -34,9 +34,11 @@
 					</div>
 					<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 active" data-tab="request-{{.ID}}">
+								{{template "shared/misc/tabtitle" (ctx.Locale.Tr "repo.settings.webhook.request")}}
+							</a>
 							<a class="item" data-tab="response-{{.ID}}">
-								{{ctx.Locale.Tr "repo.settings.webhook.response"}}
+								{{template "shared/misc/tabtitle" (ctx.Locale.Tr "repo.settings.webhook.response")}}
 								{{if .ResponseInfo}}
 									{{if .IsSucceed}}
 										<span class="ui green label">{{.ResponseInfo.Status}}</span>
@@ -49,10 +51,10 @@
 							</a>
 							{{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin $.PageIsUserSettings}}
 							<div class="right menu">
-								<form class="item" action="{{$.Link}}/replay/{{.UUID}}" method="post">
+								<form class="tw-py-2" action="{{$.Link}}/replay/{{.UUID}}" method="post">
 									{{$.CsrfTokenHtml}}
 									<span data-tooltip-content="{{if $.Webhook.IsActive}}{{ctx.Locale.Tr "repo.settings.webhook.replay.description"}}{{else}}{{ctx.Locale.Tr "repo.settings.webhook.replay.description_disabled"}}{{end}}">
-										<button class="ui tiny button{{if not $.Webhook.IsActive}} disabled{{end}}">{{svg "octicon-sync"}}</button>
+										<button class="ui tiny button tw-mr-0{{if not $.Webhook.IsActive}} disabled{{end}}">{{svg "octicon-sync"}}</button>
 									</span>
 								</form>
 							</div>
diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl
index 5bb71e7cd4..a0145ab297 100644
--- a/templates/shared/combomarkdowneditor.tmpl
+++ b/templates/shared/combomarkdowneditor.tmpl
@@ -14,8 +14,8 @@ Template Attributes:
 <div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{.ContainerClasses}}" data-dropzone-parent-container="{{.DropzoneParentContainer}}">
 	{{if .MarkdownPreviewUrl}}
 	<div class="ui top tabular menu">
-		<a class="active item" data-tab-for="markdown-writer">{{ctx.Locale.Tr "write"}}</a>
-		<a class="item" data-tab-for="markdown-previewer" data-preview-url="{{.MarkdownPreviewUrl}}" data-preview-context="{{.MarkdownPreviewContext}}">{{ctx.Locale.Tr "preview"}}</a>
+		<a class="active item" data-tab-for="markdown-writer">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "write")}}</a>
+		<a class="item" data-tab-for="markdown-previewer" data-preview-url="{{.MarkdownPreviewUrl}}" data-preview-context="{{.MarkdownPreviewContext}}">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "preview")}}</a>
 	</div>
 	{{end}}
 	<div class="ui tab active" data-tab-panel="markdown-writer">
diff --git a/templates/shared/misc/tabtitle.tmpl b/templates/shared/misc/tabtitle.tmpl
new file mode 100644
index 0000000000..dea9d4d757
--- /dev/null
+++ b/templates/shared/misc/tabtitle.tmpl
@@ -0,0 +1 @@
+<span class="resize-for-semibold" data-text="{{.}}">{{.}}</span>
diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css
index c9df6ab3f5..4a442c35b1 100644
--- a/web_src/css/modules/container.css
+++ b/web_src/css/modules/container.css
@@ -12,10 +12,6 @@
   width: 100%;
 }
 
-.ui.container.fluid.padded {
-  padding: 0 var(--page-margin-x);
-}
-
 .ui[class*="center aligned"].container {
   text-align: center;
 }

From 072b029b336a3d12c40060e8472373fded676dc2 Mon Sep 17 00:00:00 2001
From: delvh <dev.lh@web.de>
Date: Mon, 27 May 2024 10:24:34 +0200
Subject: [PATCH 045/131] Simplify review UI (#31062)

Instead of always displaying all available actions as buttons, merge
them into a single dropdown menu, same as GitHub. That decreases visual
overload and is more mobile-friendly, while not losing any
functionality.

## Screenshots
<details><summary>Before</summary>

![grafik](https://github.com/go-gitea/gitea/assets/51889757/b957fab0-4cc7-4cf5-a6c8-33f571be7b19)
</details>
<details><summary>After (unexpanded)</summary>


![grafik](https://github.com/go-gitea/gitea/assets/51889757/c8fd3428-4092-4295-bd55-c243409ba90d)
</details>

<details><summary>After (expanded)</summary>

![grafik](https://github.com/go-gitea/gitea/assets/51889757/c0eada91-54be-42ce-9db1-0db56d971438)
</details>
---
 templates/repo/diff/box.tmpl | 33 +++++++++++++++++++--------------
 1 file changed, 19 insertions(+), 14 deletions(-)

diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 641de294fd..daacdf4ba0 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -159,25 +159,30 @@
 								{{if and $isReviewFile $file.HasChangedSinceLastReview}}
 									<span class="changed-since-last-review unselectable not-mobile">{{ctx.Locale.Tr "repo.pulls.has_changed_since_last_review"}}</span>
 								{{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 tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
-								{{end}}
-								{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
-									{{if $file.IsDeleted}}
-										<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}}
 									<label data-link="{{$.Issue.Link}}/viewed-files" data-headcommit="{{$.AfterCommitID}}" class="viewed-file-form unselectable{{if $file.IsViewed}} viewed-file-checked-form{{end}}">
 										<input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{ctx.Locale.Tr "repo.pulls.has_viewed_file"}}
 									</label>
 								{{end}}
+								<div class="ui dropdown basic">
+									{{svg "octicon-kebab-horizontal" 18 "icon tw-mx-2"}}
+									<div class="ui menu">
+										{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
+											<button class="unescape-button item">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
+											<button class="escape-button tw-hidden item">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
+										{{end}}
+										{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
+											{{if $file.IsDeleted}}
+												<a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
+											{{else}}
+												<a class="item" 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="item" 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}}
+									</div>
+								</div>
 							</div>
 						</h4>
 						<div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}>

From 98751108b11dc748cc99230ca0fc1acfdf2c8929 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 27 May 2024 16:59:54 +0800
Subject: [PATCH 046/131] Rename project board -> column to make the UI less
 confusing (#30170)

This PR split the `Board` into two parts. One is the struct has been
renamed to `Column` and the second we have a `Template Type`.

But to make it easier to review, this PR will not change the database
schemas, they are just renames. The database schema changes could be in
future PRs.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: yp05327 <576951401@qq.com>
---
 .../config-cheat-sheet.en-us.md               |   2 +-
 docs/content/index.en-us.md                   |   2 +-
 docs/content/installation/comparison.en-us.md |   2 +-
 docs/content/usage/permissions.en-us.md       |   2 +-
 models/activities/statistic.go                |   4 +-
 models/issues/comment.go                      |   6 +-
 models/issues/issue_project.go                |  38 +-
 models/issues/issue_search.go                 |  14 +-
 models/migrations/v1_22/v293_test.go          |  24 +-
 models/project/board.go                       | 389 ------------------
 models/project/column.go                      | 359 ++++++++++++++++
 .../project/{board_test.go => column_test.go} |  48 +--
 models/project/issue.go                       |  24 +-
 models/project/project.go                     |  97 ++---
 models/project/project_test.go                |  14 +-
 models/project/template.go                    |  45 ++
 models/unit/unit.go                           |   2 +-
 modules/indexer/issues/bleve/bleve.go         |   4 +-
 modules/indexer/issues/db/options.go          |   2 +-
 modules/indexer/issues/dboptions.go           |   2 +-
 .../issues/elasticsearch/elasticsearch.go     |   4 +-
 modules/indexer/issues/indexer_test.go        |   4 +-
 modules/indexer/issues/internal/model.go      |   6 +-
 .../indexer/issues/internal/tests/tests.go    |  18 +-
 .../indexer/issues/meilisearch/meilisearch.go |   4 +-
 modules/indexer/issues/util.go                |   2 +-
 modules/metrics/collector.go                  |  14 +-
 options/locale/locale_en-US.ini               |   4 +-
 routers/web/org/projects.go                   | 114 +++--
 routers/web/org/projects_test.go              |   8 +-
 routers/web/repo/issue.go                     |  12 +-
 routers/web/repo/projects.go                  | 118 +++---
 routers/web/repo/projects_test.go             |   8 +-
 routers/web/web.go                            |  20 +-
 services/forms/repo_form.go                   |  50 +--
 services/forms/user_form_hidden_comments.go   |   2 +-
 templates/projects/new.tmpl                   |   6 +-
 templates/repo/header.tmpl                    |   2 +-
 templates/repo/issue/filter_actions.tmpl      |   2 +-
 templates/repo/settings/options.tmpl          |   2 +-
 tests/integration/project_test.go             |  14 +-
 web_src/css/features/projects.css             |   2 +-
 web_src/css/themes/theme-gitea-dark.css       |   2 +-
 web_src/css/themes/theme-gitea-light.css      |   2 +-
 44 files changed, 725 insertions(+), 775 deletions(-)
 delete mode 100644 models/project/board.go
 create mode 100644 models/project/column.go
 rename models/project/{board_test.go => column_test.go} (69%)
 create mode 100644 models/project/template.go

diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 9ac1f5eb10..1165a83e25 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -828,7 +828,7 @@ and
 
 ## Project (`project`)
 
-Default templates for project boards:
+Default templates for project board view:
 
 - `PROJECT_BOARD_BASIC_KANBAN_TYPE`: **To Do, In Progress, Done**
 - `PROJECT_BOARD_BUG_TRIAGE_TYPE`: **Needs Triage, High Priority, Low Priority, Closed**
diff --git a/docs/content/index.en-us.md b/docs/content/index.en-us.md
index 170bf26f71..f9e6df8c1e 100644
--- a/docs/content/index.en-us.md
+++ b/docs/content/index.en-us.md
@@ -37,7 +37,7 @@ You can try it out using [the online demo](https://try.gitea.io/).
 
 - CI/CD: Gitea Actions supports CI/CD functionality, compatible with GitHub Actions. Users can write workflows in familiar YAML format and reuse a variety of existing Actions plugins. Actions plugins support downloading from any Git website.
 
-- Project Management: Gitea tracks project requirements, features, and bugs through boards and issues. Issues support features like branches, tags, milestones, assignments, time tracking, due dates, dependencies, and more.
+- Project Management: Gitea tracks project requirements, features, and bugs through columns and issues. Issues support features like branches, tags, milestones, assignments, time tracking, due dates, dependencies, and more.
 
 - Artifact Repository: Gitea supports over 20 different types of public or private software package management, including Cargo, Chef, Composer, Conan, Conda, Container, Helm, Maven, npm, NuGet, Pub, PyPI, RubyGems, Vagrant, and more.
 
diff --git a/docs/content/installation/comparison.en-us.md b/docs/content/installation/comparison.en-us.md
index 3fb6561f31..fdb8c3bcde 100644
--- a/docs/content/installation/comparison.en-us.md
+++ b/docs/content/installation/comparison.en-us.md
@@ -104,7 +104,7 @@ _Symbols used in table:_
 | Comment reactions             | ✓                                                   | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | ✘            |
 | Lock Discussion               | ✓                                                   | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | ✘            |
 | Batch issue handling          | ✓                                                   | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | ✘            |
-| Issue Boards (Kanban)         | [/](https://github.com/go-gitea/gitea/issues/14710) | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | ✘            |
+| Projects                      | [/](https://github.com/go-gitea/gitea/issues/14710) | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | ✘            |
 | Create branch from issue      | [✘](https://github.com/go-gitea/gitea/issues/20226) | ✘    | ✘         | ✓         | ✓         | ✘         | ✘            | ✘            |
 | Convert comment to new issue  | ✓                                                   | ✘    | ✓         | ✓         | ✓         | ✘         | ✘            | ✘            |
 | Issue search                  | ✓                                                   | ✘    | ✓         | ✓         | ✓         | ✓         | ✘            | ✘            |
diff --git a/docs/content/usage/permissions.en-us.md b/docs/content/usage/permissions.en-us.md
index 1e0c6c0bb1..e4bef138ab 100644
--- a/docs/content/usage/permissions.en-us.md
+++ b/docs/content/usage/permissions.en-us.md
@@ -48,7 +48,7 @@ With different permissions, people could do different things with these units.
 | Wiki            | View wiki pages. Clone the wiki repository.        | Create/Edit wiki pages, push | -                         |
 | ExternalWiki    | Link to an external wiki                           | -                            | -                         |
 | ExternalTracker | Link to an external issue tracker                  | -                            | -                         |
-| Projects        | View the boards                                    | Change issues across boards  | -                         |
+| Projects        | View the columns of projects                       | Change issues across columns | -                         |
 | Packages        | View the packages                                  | Upload/Delete packages       | -                         |
 | Actions         | View the Actions logs                              | Approve / Cancel / Restart   | -                         |
 | Settings        | -                                                  | -                            | Manage the repository     |
diff --git a/models/activities/statistic.go b/models/activities/statistic.go
index d1a459d1b2..ff81ad78a1 100644
--- a/models/activities/statistic.go
+++ b/models/activities/statistic.go
@@ -30,7 +30,7 @@ type Statistic struct {
 		Mirror, Release, AuthSource, Webhook,
 		Milestone, Label, HookTask,
 		Team, UpdateTask, Project,
-		ProjectBoard, Attachment,
+		ProjectColumn, Attachment,
 		Branches, Tags, CommitStatus int64
 		IssueByLabel      []IssueByLabelCount
 		IssueByRepository []IssueByRepositoryCount
@@ -115,6 +115,6 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
 	stats.Counter.Team, _ = e.Count(new(organization.Team))
 	stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
 	stats.Counter.Project, _ = e.Count(new(project_model.Project))
-	stats.Counter.ProjectBoard, _ = e.Count(new(project_model.Board))
+	stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column))
 	return stats
 }
diff --git a/models/issues/comment.go b/models/issues/comment.go
index 353163ebd6..336bdde58e 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -100,8 +100,8 @@ const (
 	CommentTypeMergePull       // 28 merge pull request
 	CommentTypePullRequestPush // 29 push to PR head branch
 
-	CommentTypeProject      // 30 Project changed
-	CommentTypeProjectBoard // 31 Project board changed
+	CommentTypeProject       // 30 Project changed
+	CommentTypeProjectColumn // 31 Project column changed
 
 	CommentTypeDismissReview // 32 Dismiss Review
 
@@ -146,7 +146,7 @@ var commentStrings = []string{
 	"merge_pull",
 	"pull_push",
 	"project",
-	"project_board",
+	"project_board", // FIXME: the name should be project_column
 	"dismiss_review",
 	"change_issue_ref",
 	"pull_scheduled_merge",
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index e31d2ef151..835ea1db52 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -37,22 +37,22 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
 	return ip.ProjectID
 }
 
-// ProjectBoardID return project board id if issue was assigned to one
-func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
+// ProjectColumnID return project column id if issue was assigned to one
+func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
 	var ip project_model.ProjectIssue
 	has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
 	if err != nil || !has {
 		return 0
 	}
-	return ip.ProjectBoardID
+	return ip.ProjectColumnID
 }
 
-// LoadIssuesFromBoard load issues assigned to this board
-func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
+// LoadIssuesFromColumn load issues assigned to this column
+func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) {
 	issueList, err := Issues(ctx, &IssuesOptions{
-		ProjectBoardID: b.ID,
-		ProjectID:      b.ProjectID,
-		SortType:       "project-column-sorting",
+		ProjectColumnID: b.ID,
+		ProjectID:       b.ProjectID,
+		SortType:        "project-column-sorting",
 	})
 	if err != nil {
 		return nil, err
@@ -60,9 +60,9 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
 
 	if b.Default {
 		issues, err := Issues(ctx, &IssuesOptions{
-			ProjectBoardID: db.NoConditionID,
-			ProjectID:      b.ProjectID,
-			SortType:       "project-column-sorting",
+			ProjectColumnID: db.NoConditionID,
+			ProjectID:       b.ProjectID,
+			SortType:        "project-column-sorting",
 		})
 		if err != nil {
 			return nil, err
@@ -77,11 +77,11 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
 	return issueList, nil
 }
 
-// LoadIssuesFromBoardList load issues assigned to the boards
-func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) {
+// LoadIssuesFromColumnList load issues assigned to the columns
+func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) {
 	issuesMap := make(map[int64]IssueList, len(bs))
 	for i := range bs {
-		il, err := LoadIssuesFromBoard(ctx, bs[i])
+		il, err := LoadIssuesFromColumn(ctx, bs[i])
 		if err != nil {
 			return nil, err
 		}
@@ -110,7 +110,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
 				return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
 			}
 			if newColumnID == 0 {
-				newDefaultColumn, err := newProject.GetDefaultBoard(ctx)
+				newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
 				if err != nil {
 					return err
 				}
@@ -153,10 +153,10 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
 		}
 		newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
 		return db.Insert(ctx, &project_model.ProjectIssue{
-			IssueID:        issue.ID,
-			ProjectID:      newProjectID,
-			ProjectBoardID: newColumnID,
-			Sorting:        newSorting,
+			IssueID:         issue.ID,
+			ProjectID:       newProjectID,
+			ProjectColumnID: newColumnID,
+			Sorting:         newSorting,
 		})
 	})
 }
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index 921dd9973e..491def1229 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -33,7 +33,7 @@ type IssuesOptions struct { //nolint
 	SubscriberID       int64
 	MilestoneIDs       []int64
 	ProjectID          int64
-	ProjectBoardID     int64
+	ProjectColumnID    int64
 	IsClosed           optional.Option[bool]
 	IsPull             optional.Option[bool]
 	LabelIDs           []int64
@@ -169,12 +169,12 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
 	return sess
 }
 
-func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
-	// opts.ProjectBoardID == 0 means all project boards,
+func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+	// opts.ProjectColumnID == 0 means all project columns,
 	// do not need to apply any condition
-	if opts.ProjectBoardID > 0 {
-		sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID}))
-	} else if opts.ProjectBoardID == db.NoConditionID {
+	if opts.ProjectColumnID > 0 {
+		sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID}))
+	} else if opts.ProjectColumnID == db.NoConditionID {
 		sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
 	}
 	return sess
@@ -246,7 +246,7 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 
 	applyProjectCondition(sess, opts)
 
-	applyProjectBoardCondition(sess, opts)
+	applyProjectColumnCondition(sess, opts)
 
 	if opts.IsPull.Has() {
 		sess.And("issue.is_pull=?", opts.IsPull.Value())
diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go
index ccc92f39a6..cfe4345143 100644
--- a/models/migrations/v1_22/v293_test.go
+++ b/models/migrations/v1_22/v293_test.go
@@ -15,7 +15,7 @@ import (
 
 func Test_CheckProjectColumnsConsistency(t *testing.T) {
 	// Prepare and load the testing database
-	x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board))
+	x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column))
 	defer deferable()
 	if x == nil || t.Failed() {
 		return
@@ -23,22 +23,22 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) {
 
 	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)
+	// check if default column was added
+	var defaultColumn project.Column
+	has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultColumn)
 	assert.NoError(t, err)
 	assert.True(t, has)
-	assert.Equal(t, int64(1), defaultBoard.ProjectID)
-	assert.True(t, defaultBoard.Default)
+	assert.Equal(t, int64(1), defaultColumn.ProjectID)
+	assert.True(t, defaultColumn.Default)
 
 	// check if multiple defaults, previous were removed and last will be kept
-	expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
+	expectDefaultColumn, err := project.GetColumn(db.DefaultContext, 2)
 	assert.NoError(t, err)
-	assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
-	assert.False(t, expectDefaultBoard.Default)
+	assert.Equal(t, int64(2), expectDefaultColumn.ProjectID)
+	assert.False(t, expectDefaultColumn.Default)
 
-	expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
+	expectNonDefaultColumn, err := project.GetColumn(db.DefaultContext, 3)
 	assert.NoError(t, err)
-	assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
-	assert.True(t, expectNonDefaultBoard.Default)
+	assert.Equal(t, int64(2), expectNonDefaultColumn.ProjectID)
+	assert.True(t, expectNonDefaultColumn.Default)
 }
diff --git a/models/project/board.go b/models/project/board.go
deleted file mode 100644
index a52baa0c18..0000000000
--- a/models/project/board.go
+++ /dev/null
@@ -1,389 +0,0 @@
-// Copyright 2020 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package project
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"regexp"
-
-	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
-
-	"xorm.io/builder"
-)
-
-type (
-	// BoardType is used to represent a project board type
-	BoardType uint8
-
-	// CardType is used to represent a project board card type
-	CardType uint8
-
-	// BoardList is a list of all project boards in a repository
-	BoardList []*Board
-)
-
-const (
-	// BoardTypeNone is a project board type that has no predefined columns
-	BoardTypeNone BoardType = iota
-
-	// BoardTypeBasicKanban is a project board type that has basic predefined columns
-	BoardTypeBasicKanban
-
-	// BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs
-	BoardTypeBugTriage
-)
-
-const (
-	// CardTypeTextOnly is a project board card type that is text only
-	CardTypeTextOnly CardType = iota
-
-	// CardTypeImagesAndText is a project board card type that has images and text
-	CardTypeImagesAndText
-)
-
-// BoardColorPattern is a regexp witch can validate BoardColor
-var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
-
-// Board is used to represent boards on a project
-type Board 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"`
-}
-
-// TableName return the real table name
-func (Board) TableName() string {
-	return "project_board"
-}
-
-// NumIssues return counter of all issues assigned to the board
-func (b *Board) NumIssues(ctx context.Context) int {
-	c, err := db.GetEngine(ctx).Table("project_issue").
-		Where("project_id=?", b.ProjectID).
-		And("project_board_id=?", b.ID).
-		GroupBy("issue_id").
-		Cols("issue_id").
-		Count()
-	if err != nil {
-		return 0
-	}
-	return int(c)
-}
-
-func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
-	issues := make([]*ProjectIssue, 0, 5)
-	if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID).
-		And("project_board_id=?", b.ID).
-		OrderBy("sorting, id").
-		Find(&issues); err != nil {
-		return nil, err
-	}
-	return issues, nil
-}
-
-func init() {
-	db.RegisterModel(new(Board))
-}
-
-// IsBoardTypeValid checks if the project board type is valid
-func IsBoardTypeValid(p BoardType) bool {
-	switch p {
-	case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage:
-		return true
-	default:
-		return false
-	}
-}
-
-// IsCardTypeValid checks if the project board card type is valid
-func IsCardTypeValid(p CardType) bool {
-	switch p {
-	case CardTypeTextOnly, CardTypeImagesAndText:
-		return true
-	default:
-		return false
-	}
-}
-
-func createBoardsForProjectsType(ctx context.Context, project *Project) error {
-	var items []string
-
-	switch project.BoardType {
-	case BoardTypeBugTriage:
-		items = setting.Project.ProjectBoardBugTriageType
-
-	case BoardTypeBasicKanban:
-		items = setting.Project.ProjectBoardBasicKanbanType
-	case BoardTypeNone:
-		fallthrough
-	default:
-		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
-	}
-
-	boards := make([]Board, 0, len(items))
-
-	for _, v := range items {
-		boards = append(boards, Board{
-			CreatedUnix: timeutil.TimeStampNow(),
-			CreatorID:   project.CreatorID,
-			Title:       v,
-			ProjectID:   project.ID,
-		})
-	}
-
-	return db.Insert(ctx, boards)
-}
-
-// maxProjectColumns max columns allowed in a project, this should not bigger than 127
-// because sorting is int8 in database
-const maxProjectColumns = 20
-
-// NewBoard adds a new project board to a given project
-func NewBoard(ctx context.Context, board *Board) error {
-	if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
-		return fmt.Errorf("bad color code: %s", board.Color)
-	}
-	res := struct {
-		MaxSorting  int64
-		ColumnCount int64
-	}{}
-	if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
-		Where("project_id=?", board.ProjectID).Get(&res); err != nil {
-		return err
-	}
-	if res.ColumnCount >= maxProjectColumns {
-		return fmt.Errorf("NewBoard: maximum number of columns reached")
-	}
-	board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
-	_, err := db.GetEngine(ctx).Insert(board)
-	return err
-}
-
-// DeleteBoardByID removes all issues references to the project board.
-func DeleteBoardByID(ctx context.Context, boardID int64) error {
-	ctx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := deleteBoardByID(ctx, boardID); err != nil {
-		return err
-	}
-
-	return committer.Commit()
-}
-
-func deleteBoardByID(ctx context.Context, boardID int64) error {
-	board, err := GetBoard(ctx, boardID)
-	if err != nil {
-		if IsErrProjectBoardNotExist(err) {
-			return nil
-		}
-
-		return err
-	}
-
-	if board.Default {
-		return fmt.Errorf("deleteBoardByID: cannot delete default board")
-	}
-
-	// move all issues to the default column
-	project, err := GetProjectByID(ctx, board.ProjectID)
-	if err != nil {
-		return err
-	}
-	defaultColumn, err := project.GetDefaultBoard(ctx)
-	if err != nil {
-		return err
-	}
-
-	if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
-		return err
-	}
-
-	if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil {
-		return err
-	}
-	return nil
-}
-
-func deleteBoardByProjectID(ctx context.Context, projectID int64) error {
-	_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{})
-	return err
-}
-
-// 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
-	} else if !has {
-		return nil, ErrProjectBoardNotExist{BoardID: boardID}
-	}
-
-	return board, nil
-}
-
-// UpdateBoard updates a project board
-func UpdateBoard(ctx context.Context, board *Board) error {
-	var fieldToUpdate []string
-
-	if board.Sorting != 0 {
-		fieldToUpdate = append(fieldToUpdate, "sorting")
-	}
-
-	if board.Title != "" {
-		fieldToUpdate = append(fieldToUpdate, "title")
-	}
-
-	if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
-		return fmt.Errorf("bad color code: %s", board.Color)
-	}
-	fieldToUpdate = append(fieldToUpdate, "color")
-
-	_, err := db.GetEngine(ctx).ID(board.ID).Cols(fieldToUpdate...).Update(board)
-
-	return err
-}
-
-// GetBoards fetches all boards related to a project
-func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
-	boards := make([]*Board, 0, 5)
-	if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil {
-		return nil, err
-	}
-
-	return boards, nil
-}
-
-// GetDefaultBoard return default board and ensure only one exists
-func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
-	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
-	}
-
-	if has {
-		return &board, nil
-	}
-
-	// create a default board if none is found
-	board = Board{
-		ProjectID: p.ID,
-		Default:   true,
-		Title:     "Uncategorized",
-		CreatorID: p.CreatorID,
-	}
-	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 {
-	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
-	})
-}
-
-// UpdateBoardSorting update project board sorting
-func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
-	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
-	})
-}
-
-func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) {
-	columns := make([]*Board, 0, 5)
-	if err := db.GetEngine(ctx).
-		Where("project_id =?", projectID).
-		In("id", columnsIDs).
-		OrderBy("sorting").Find(&columns); err != nil {
-		return nil, err
-	}
-	return columns, nil
-}
-
-// MoveColumnsOnProject sorts columns in a project
-func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
-	return db.WithTx(ctx, func(ctx context.Context) error {
-		sess := db.GetEngine(ctx)
-		columnIDs := util.ValuesOfMap(sortedColumnIDs)
-		movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
-		if err != nil {
-			return err
-		}
-		if len(movedColumns) != len(sortedColumnIDs) {
-			return errors.New("some columns do not exist")
-		}
-
-		for _, column := range movedColumns {
-			if column.ProjectID != project.ID {
-				return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
-			}
-		}
-
-		for sorting, columnID := range sortedColumnIDs {
-			if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
-				return err
-			}
-		}
-		return nil
-	})
-}
diff --git a/models/project/column.go b/models/project/column.go
new file mode 100644
index 0000000000..222f448599
--- /dev/null
+++ b/models/project/column.go
@@ -0,0 +1,359 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package project
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"regexp"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
+
+	"xorm.io/builder"
+)
+
+type (
+
+	// CardType is used to represent a project column card type
+	CardType uint8
+
+	// ColumnList is a list of all project columns in a repository
+	ColumnList []*Column
+)
+
+const (
+	// CardTypeTextOnly is a project column card type that is text only
+	CardTypeTextOnly CardType = iota
+
+	// CardTypeImagesAndText is a project column card type that has images and text
+	CardTypeImagesAndText
+)
+
+// ColumnColorPattern is a regexp witch can validate ColumnColor
+var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
+
+// Column is used to represent column on a project
+type Column struct {
+	ID      int64 `xorm:"pk autoincr"`
+	Title   string
+	Default bool   `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific column will be assigned to this column
+	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"`
+}
+
+// TableName return the real table name
+func (Column) TableName() string {
+	return "project_board" // TODO: the legacy table name should be project_column
+}
+
+// NumIssues return counter of all issues assigned to the column
+func (c *Column) NumIssues(ctx context.Context) int {
+	total, err := db.GetEngine(ctx).Table("project_issue").
+		Where("project_id=?", c.ProjectID).
+		And("project_board_id=?", c.ID).
+		GroupBy("issue_id").
+		Cols("issue_id").
+		Count()
+	if err != nil {
+		return 0
+	}
+	return int(total)
+}
+
+func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
+	issues := make([]*ProjectIssue, 0, 5)
+	if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
+		And("project_board_id=?", c.ID).
+		OrderBy("sorting, id").
+		Find(&issues); err != nil {
+		return nil, err
+	}
+	return issues, nil
+}
+
+func init() {
+	db.RegisterModel(new(Column))
+}
+
+// IsCardTypeValid checks if the project column card type is valid
+func IsCardTypeValid(p CardType) bool {
+	switch p {
+	case CardTypeTextOnly, CardTypeImagesAndText:
+		return true
+	default:
+		return false
+	}
+}
+
+func createDefaultColumnsForProject(ctx context.Context, project *Project) error {
+	var items []string
+
+	switch project.TemplateType {
+	case TemplateTypeBugTriage:
+		items = setting.Project.ProjectBoardBugTriageType
+	case TemplateTypeBasicKanban:
+		items = setting.Project.ProjectBoardBasicKanbanType
+	case TemplateTypeNone:
+		fallthrough
+	default:
+		return nil
+	}
+
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		column := Column{
+			CreatedUnix: timeutil.TimeStampNow(),
+			CreatorID:   project.CreatorID,
+			Title:       "Backlog",
+			ProjectID:   project.ID,
+			Default:     true,
+		}
+		if err := db.Insert(ctx, column); err != nil {
+			return err
+		}
+
+		if len(items) == 0 {
+			return nil
+		}
+
+		columns := make([]Column, 0, len(items))
+		for _, v := range items {
+			columns = append(columns, Column{
+				CreatedUnix: timeutil.TimeStampNow(),
+				CreatorID:   project.CreatorID,
+				Title:       v,
+				ProjectID:   project.ID,
+			})
+		}
+
+		return db.Insert(ctx, columns)
+	})
+}
+
+// maxProjectColumns max columns allowed in a project, this should not bigger than 127
+// because sorting is int8 in database
+const maxProjectColumns = 20
+
+// NewColumn adds a new project column to a given project
+func NewColumn(ctx context.Context, column *Column) error {
+	if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
+		return fmt.Errorf("bad color code: %s", column.Color)
+	}
+
+	res := struct {
+		MaxSorting  int64
+		ColumnCount int64
+	}{}
+	if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
+		Where("project_id=?", column.ProjectID).Get(&res); err != nil {
+		return err
+	}
+	if res.ColumnCount >= maxProjectColumns {
+		return fmt.Errorf("NewBoard: maximum number of columns reached")
+	}
+	column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
+	_, err := db.GetEngine(ctx).Insert(column)
+	return err
+}
+
+// DeleteColumnByID removes all issues references to the project column.
+func DeleteColumnByID(ctx context.Context, columnID int64) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		return deleteColumnByID(ctx, columnID)
+	})
+}
+
+func deleteColumnByID(ctx context.Context, columnID int64) error {
+	column, err := GetColumn(ctx, columnID)
+	if err != nil {
+		if IsErrProjectColumnNotExist(err) {
+			return nil
+		}
+
+		return err
+	}
+
+	if column.Default {
+		return fmt.Errorf("deleteColumnByID: cannot delete default column")
+	}
+
+	// move all issues to the default column
+	project, err := GetProjectByID(ctx, column.ProjectID)
+	if err != nil {
+		return err
+	}
+	defaultColumn, err := project.GetDefaultColumn(ctx)
+	if err != nil {
+		return err
+	}
+
+	if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
+		return err
+	}
+
+	if _, err := db.GetEngine(ctx).ID(column.ID).NoAutoCondition().Delete(column); err != nil {
+		return err
+	}
+	return nil
+}
+
+func deleteColumnByProjectID(ctx context.Context, projectID int64) error {
+	_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Column{})
+	return err
+}
+
+// GetColumn fetches the current column of a project
+func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
+	column := new(Column)
+	has, err := db.GetEngine(ctx).ID(columnID).Get(column)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrProjectColumnNotExist{ColumnID: columnID}
+	}
+
+	return column, nil
+}
+
+// UpdateColumn updates a project column
+func UpdateColumn(ctx context.Context, column *Column) error {
+	var fieldToUpdate []string
+
+	if column.Sorting != 0 {
+		fieldToUpdate = append(fieldToUpdate, "sorting")
+	}
+
+	if column.Title != "" {
+		fieldToUpdate = append(fieldToUpdate, "title")
+	}
+
+	if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
+		return fmt.Errorf("bad color code: %s", column.Color)
+	}
+	fieldToUpdate = append(fieldToUpdate, "color")
+
+	_, err := db.GetEngine(ctx).ID(column.ID).Cols(fieldToUpdate...).Update(column)
+
+	return err
+}
+
+// GetColumns fetches all columns related to a project
+func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
+	columns := make([]*Column, 0, 5)
+	if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&columns); err != nil {
+		return nil, err
+	}
+
+	return columns, nil
+}
+
+// GetDefaultColumn return default column and ensure only one exists
+func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
+	var column Column
+	has, err := db.GetEngine(ctx).
+		Where("project_id=? AND `default` = ?", p.ID, true).
+		Desc("id").Get(&column)
+	if err != nil {
+		return nil, err
+	}
+
+	if has {
+		return &column, nil
+	}
+
+	// create a default column if none is found
+	column = Column{
+		ProjectID: p.ID,
+		Default:   true,
+		Title:     "Uncategorized",
+		CreatorID: p.CreatorID,
+	}
+	if _, err := db.GetEngine(ctx).Insert(&column); err != nil {
+		return nil, err
+	}
+	return &column, nil
+}
+
+// SetDefaultColumn represents a column for issues not assigned to one
+func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		if _, err := GetColumn(ctx, columnID); err != nil {
+			return err
+		}
+
+		if _, err := db.GetEngine(ctx).Where(builder.Eq{
+			"project_id": projectID,
+			"`default`":  true,
+		}).Cols("`default`").Update(&Column{Default: false}); err != nil {
+			return err
+		}
+
+		_, err := db.GetEngine(ctx).ID(columnID).
+			Where(builder.Eq{"project_id": projectID}).
+			Cols("`default`").Update(&Column{Default: true})
+		return err
+	})
+}
+
+// UpdateColumnSorting update project column sorting
+func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		for i := range cl {
+			if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
+				"sorting",
+			).Update(cl[i]); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
+	columns := make([]*Column, 0, 5)
+	if err := db.GetEngine(ctx).
+		Where("project_id =?", projectID).
+		In("id", columnsIDs).
+		OrderBy("sorting").Find(&columns); err != nil {
+		return nil, err
+	}
+	return columns, nil
+}
+
+// MoveColumnsOnProject sorts columns in a project
+func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		sess := db.GetEngine(ctx)
+		columnIDs := util.ValuesOfMap(sortedColumnIDs)
+		movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
+		if err != nil {
+			return err
+		}
+		if len(movedColumns) != len(sortedColumnIDs) {
+			return errors.New("some columns do not exist")
+		}
+
+		for _, column := range movedColumns {
+			if column.ProjectID != project.ID {
+				return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
+			}
+		}
+
+		for sorting, columnID := range sortedColumnIDs {
+			if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
diff --git a/models/project/board_test.go b/models/project/column_test.go
similarity index 69%
rename from models/project/board_test.go
rename to models/project/column_test.go
index da922ff7ad..911649fb72 100644
--- a/models/project/board_test.go
+++ b/models/project/column_test.go
@@ -14,48 +14,48 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestGetDefaultBoard(t *testing.T) {
+func TestGetDefaultColumn(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)
+	// check if default column was added
+	column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
 	assert.NoError(t, err)
-	assert.Equal(t, int64(5), board.ProjectID)
-	assert.Equal(t, "Uncategorized", board.Title)
+	assert.Equal(t, int64(5), column.ProjectID)
+	assert.Equal(t, "Uncategorized", column.Title)
 
 	projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
 	assert.NoError(t, err)
 
 	// check if multiple defaults were removed
-	board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext)
+	column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
 	assert.NoError(t, err)
-	assert.Equal(t, int64(6), board.ProjectID)
-	assert.Equal(t, int64(9), board.ID)
+	assert.Equal(t, int64(6), column.ProjectID)
+	assert.Equal(t, int64(9), column.ID)
 
-	// set 8 as default board
-	assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8))
+	// set 8 as default column
+	assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
 
-	// then 9 will become a non-default board
-	board, err = GetBoard(db.DefaultContext, 9)
+	// then 9 will become a non-default column
+	column, err = GetColumn(db.DefaultContext, 9)
 	assert.NoError(t, err)
-	assert.Equal(t, int64(6), board.ProjectID)
-	assert.False(t, board.Default)
+	assert.Equal(t, int64(6), column.ProjectID)
+	assert.False(t, column.Default)
 }
 
 func Test_moveIssuesToAnotherColumn(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
-	column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1})
+	column1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1})
 
 	issues, err := column1.GetIssues(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Len(t, issues, 1)
 	assert.EqualValues(t, 1, issues[0].ID)
 
-	column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1})
+	column2 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 2, ProjectID: 1})
 	issues, err = column2.GetIssues(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Len(t, issues, 1)
@@ -81,7 +81,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
-	columns, err := project1.GetBoards(db.DefaultContext)
+	columns, err := project1.GetColumns(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Len(t, columns, 3)
 	assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
@@ -95,7 +95,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
 	})
 	assert.NoError(t, err)
 
-	columnsAfter, err := project1.GetBoards(db.DefaultContext)
+	columnsAfter, err := project1.GetColumns(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Len(t, columnsAfter, 3)
 	assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
@@ -103,23 +103,23 @@ func Test_MoveColumnsOnProject(t *testing.T) {
 	assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
 }
 
-func Test_NewBoard(t *testing.T) {
+func Test_NewColumn(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
-	columns, err := project1.GetBoards(db.DefaultContext)
+	columns, err := project1.GetColumns(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Len(t, columns, 3)
 
 	for i := 0; i < maxProjectColumns-3; i++ {
-		err := NewBoard(db.DefaultContext, &Board{
-			Title:     fmt.Sprintf("board-%d", i+4),
+		err := NewColumn(db.DefaultContext, &Column{
+			Title:     fmt.Sprintf("column-%d", i+4),
 			ProjectID: project1.ID,
 		})
 		assert.NoError(t, err)
 	}
-	err = NewBoard(db.DefaultContext, &Board{
-		Title:     "board-21",
+	err = NewColumn(db.DefaultContext, &Column{
+		Title:     "column-21",
 		ProjectID: project1.ID,
 	})
 	assert.Error(t, err)
diff --git a/models/project/issue.go b/models/project/issue.go
index 32e72e909d..3361b533b9 100644
--- a/models/project/issue.go
+++ b/models/project/issue.go
@@ -18,10 +18,10 @@ type ProjectIssue struct { //revive:disable-line:exported
 	IssueID   int64 `xorm:"INDEX"`
 	ProjectID int64 `xorm:"INDEX"`
 
-	// ProjectBoardID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
-	ProjectBoardID int64 `xorm:"INDEX"`
+	// ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
+	ProjectColumnID int64 `xorm:"'project_board_id' INDEX"`
 
-	// the sorting order on the board
+	// the sorting order on the column
 	Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
 }
 
@@ -76,13 +76,13 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
 	return int(c)
 }
 
-// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
-func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error {
+// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
+func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error {
 	return db.WithTx(ctx, func(ctx context.Context) error {
 		sess := db.GetEngine(ctx)
 		issueIDs := util.ValuesOfMap(sortedIssueIDs)
 
-		count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
+		count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count()
 		if err != nil {
 			return err
 		}
@@ -91,7 +91,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
 		}
 
 		for sorting, issueID := range sortedIssueIDs {
-			_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
+			_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
 			if err != nil {
 				return err
 			}
@@ -100,12 +100,12 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
 	})
 }
 
-func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error {
-	if b.ProjectID != newColumn.ProjectID {
+func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
+	if c.ProjectID != newColumn.ProjectID {
 		return fmt.Errorf("columns have to be in the same project")
 	}
 
-	if b.ID == newColumn.ID {
+	if c.ID == newColumn.ID {
 		return nil
 	}
 
@@ -121,7 +121,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
 		return err
 	}
 
-	issues, err := b.GetIssues(ctx)
+	issues, err := c.GetIssues(ctx)
 	if err != nil {
 		return err
 	}
@@ -132,7 +132,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
 	nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
 	return db.WithTx(ctx, func(ctx context.Context) error {
 		for i, issue := range issues {
-			issue.ProjectBoardID = newColumn.ID
+			issue.ProjectColumnID = newColumn.ID
 			issue.Sorting = nextSorting + int64(i)
 			if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
 				return err
diff --git a/models/project/project.go b/models/project/project.go
index 8be38694c5..fe5d408f64 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -21,13 +21,7 @@ import (
 )
 
 type (
-	// BoardConfig is used to identify the type of board that is being created
-	BoardConfig struct {
-		BoardType   BoardType
-		Translation string
-	}
-
-	// CardConfig is used to identify the type of board card that is being used
+	// CardConfig is used to identify the type of column card that is being used
 	CardConfig struct {
 		CardType    CardType
 		Translation string
@@ -38,7 +32,7 @@ type (
 )
 
 const (
-	// TypeIndividual is a type of project board that is owned by an individual
+	// TypeIndividual is a type of project column that is owned by an individual
 	TypeIndividual Type = iota + 1
 
 	// TypeRepository is a project that is tied to a repository
@@ -68,39 +62,39 @@ func (err ErrProjectNotExist) Unwrap() error {
 	return util.ErrNotExist
 }
 
-// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
-type ErrProjectBoardNotExist struct {
-	BoardID int64
+// ErrProjectColumnNotExist represents a "ErrProjectColumnNotExist" kind of error.
+type ErrProjectColumnNotExist struct {
+	ColumnID int64
 }
 
-// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
-func IsErrProjectBoardNotExist(err error) bool {
-	_, ok := err.(ErrProjectBoardNotExist)
+// IsErrProjectColumnNotExist checks if an error is a ErrProjectColumnNotExist
+func IsErrProjectColumnNotExist(err error) bool {
+	_, ok := err.(ErrProjectColumnNotExist)
 	return ok
 }
 
-func (err ErrProjectBoardNotExist) Error() string {
-	return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID)
+func (err ErrProjectColumnNotExist) Error() string {
+	return fmt.Sprintf("project column does not exist [id: %d]", err.ColumnID)
 }
 
-func (err ErrProjectBoardNotExist) Unwrap() error {
+func (err ErrProjectColumnNotExist) Unwrap() error {
 	return util.ErrNotExist
 }
 
-// Project represents a project board
+// Project represents a project
 type Project struct {
-	ID          int64                  `xorm:"pk autoincr"`
-	Title       string                 `xorm:"INDEX NOT NULL"`
-	Description string                 `xorm:"TEXT"`
-	OwnerID     int64                  `xorm:"INDEX"`
-	Owner       *user_model.User       `xorm:"-"`
-	RepoID      int64                  `xorm:"INDEX"`
-	Repo        *repo_model.Repository `xorm:"-"`
-	CreatorID   int64                  `xorm:"NOT NULL"`
-	IsClosed    bool                   `xorm:"INDEX"`
-	BoardType   BoardType
-	CardType    CardType
-	Type        Type
+	ID           int64                  `xorm:"pk autoincr"`
+	Title        string                 `xorm:"INDEX NOT NULL"`
+	Description  string                 `xorm:"TEXT"`
+	OwnerID      int64                  `xorm:"INDEX"`
+	Owner        *user_model.User       `xorm:"-"`
+	RepoID       int64                  `xorm:"INDEX"`
+	Repo         *repo_model.Repository `xorm:"-"`
+	CreatorID    int64                  `xorm:"NOT NULL"`
+	IsClosed     bool                   `xorm:"INDEX"`
+	TemplateType TemplateType           `xorm:"'board_type'"` // TODO: rename the column to template_type
+	CardType     CardType
+	Type         Type
 
 	RenderedContent template.HTML `xorm:"-"`
 
@@ -172,16 +166,7 @@ func init() {
 	db.RegisterModel(new(Project))
 }
 
-// GetBoardConfig retrieves the types of configurations project boards could have
-func GetBoardConfig() []BoardConfig {
-	return []BoardConfig{
-		{BoardTypeNone, "repo.projects.type.none"},
-		{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
-		{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
-	}
-}
-
-// GetCardConfig retrieves the types of configurations project board cards could have
+// GetCardConfig retrieves the types of configurations project column cards could have
 func GetCardConfig() []CardConfig {
 	return []CardConfig{
 		{CardTypeTextOnly, "repo.projects.card_type.text_only"},
@@ -251,8 +236,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
 
 // NewProject creates a new Project
 func NewProject(ctx context.Context, p *Project) error {
-	if !IsBoardTypeValid(p.BoardType) {
-		p.BoardType = BoardTypeNone
+	if !IsTemplateTypeValid(p.TemplateType) {
+		p.TemplateType = TemplateTypeNone
 	}
 
 	if !IsCardTypeValid(p.CardType) {
@@ -263,27 +248,19 @@ func NewProject(ctx context.Context, p *Project) error {
 		return util.NewInvalidArgumentErrorf("project type is not valid")
 	}
 
-	ctx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := db.Insert(ctx, p); err != nil {
-		return err
-	}
-
-	if p.RepoID > 0 {
-		if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
+	return db.WithTx(ctx, func(ctx context.Context) error {
+		if err := db.Insert(ctx, p); err != nil {
 			return err
 		}
-	}
 
-	if err := createBoardsForProjectsType(ctx, p); err != nil {
-		return err
-	}
+		if p.RepoID > 0 {
+			if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
+				return err
+			}
+		}
 
-	return committer.Commit()
+		return createDefaultColumnsForProject(ctx, p)
+	})
 }
 
 // GetProjectByID returns the projects in a repository
@@ -417,7 +394,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error {
 			return err
 		}
 
-		if err := deleteBoardByProjectID(ctx, id); err != nil {
+		if err := deleteColumnByProjectID(ctx, id); err != nil {
 			return err
 		}
 
diff --git a/models/project/project_test.go b/models/project/project_test.go
index 8fbbdedecf..dd421b4659 100644
--- a/models/project/project_test.go
+++ b/models/project/project_test.go
@@ -51,13 +51,13 @@ func TestProject(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	project := &Project{
-		Type:        TypeRepository,
-		BoardType:   BoardTypeBasicKanban,
-		CardType:    CardTypeTextOnly,
-		Title:       "New Project",
-		RepoID:      1,
-		CreatedUnix: timeutil.TimeStampNow(),
-		CreatorID:   2,
+		Type:         TypeRepository,
+		TemplateType: TemplateTypeBasicKanban,
+		CardType:     CardTypeTextOnly,
+		Title:        "New Project",
+		RepoID:       1,
+		CreatedUnix:  timeutil.TimeStampNow(),
+		CreatorID:    2,
 	}
 
 	assert.NoError(t, NewProject(db.DefaultContext, project))
diff --git a/models/project/template.go b/models/project/template.go
new file mode 100644
index 0000000000..06d5d2af14
--- /dev/null
+++ b/models/project/template.go
@@ -0,0 +1,45 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package project
+
+type (
+	// TemplateType is used to represent a project template type
+	TemplateType uint8
+
+	// TemplateConfig is used to identify the template type of project that is being created
+	TemplateConfig struct {
+		TemplateType TemplateType
+		Translation  string
+	}
+)
+
+const (
+	// TemplateTypeNone is a project template type that has no predefined columns
+	TemplateTypeNone TemplateType = iota
+
+	// TemplateTypeBasicKanban is a project template type that has basic predefined columns
+	TemplateTypeBasicKanban
+
+	// TemplateTypeBugTriage is a project template type that has predefined columns suited to hunting down bugs
+	TemplateTypeBugTriage
+)
+
+// GetTemplateConfigs retrieves the template configs of configurations project columns could have
+func GetTemplateConfigs() []TemplateConfig {
+	return []TemplateConfig{
+		{TemplateTypeNone, "repo.projects.type.none"},
+		{TemplateTypeBasicKanban, "repo.projects.type.basic_kanban"},
+		{TemplateTypeBugTriage, "repo.projects.type.bug_triage"},
+	}
+}
+
+// IsTemplateTypeValid checks if the project template type is valid
+func IsTemplateTypeValid(p TemplateType) bool {
+	switch p {
+	case TemplateTypeNone, TemplateTypeBasicKanban, TemplateTypeBugTriage:
+		return true
+	default:
+		return false
+	}
+}
diff --git a/models/unit/unit.go b/models/unit/unit.go
index 74efa4caf0..8eedcbd347 100644
--- a/models/unit/unit.go
+++ b/models/unit/unit.go
@@ -28,7 +28,7 @@ const (
 	TypeWiki                        // 5 Wiki
 	TypeExternalWiki                // 6 ExternalWiki
 	TypeExternalTracker             // 7 ExternalTracker
-	TypeProjects                    // 8 Kanban board
+	TypeProjects                    // 8 Projects
 	TypePackages                    // 9 Packages
 	TypeActions                     // 10 Actions
 )
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index d7957b266a..7ef370e89c 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -224,8 +224,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	if options.ProjectID.Has() {
 		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
 	}
-	if options.ProjectBoardID.Has() {
-		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id"))
+	if options.ProjectColumnID.Has() {
+		queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
 	}
 
 	if options.PosterID.Has() {
diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go
index eeaf1696ad..875a4ca279 100644
--- a/modules/indexer/issues/db/options.go
+++ b/modules/indexer/issues/db/options.go
@@ -61,7 +61,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 		ReviewedID:         convertID(options.ReviewedID),
 		SubscriberID:       convertID(options.SubscriberID),
 		ProjectID:          convertID(options.ProjectID),
-		ProjectBoardID:     convertID(options.ProjectBoardID),
+		ProjectColumnID:    convertID(options.ProjectColumnID),
 		IsClosed:           options.IsClosed,
 		IsPull:             options.IsPull,
 		IncludedLabelNames: nil,
diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go
index 8f94088742..d9cf9b5e3b 100644
--- a/modules/indexer/issues/dboptions.go
+++ b/modules/indexer/issues/dboptions.go
@@ -50,7 +50,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
 	}
 
 	searchOpt.ProjectID = convertID(opts.ProjectID)
-	searchOpt.ProjectBoardID = convertID(opts.ProjectBoardID)
+	searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
 	searchOpt.PosterID = convertID(opts.PosterID)
 	searchOpt.AssigneeID = convertID(opts.AssigneeID)
 	searchOpt.MentionID = convertID(opts.MentionedID)
diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go
index c7cb59f2cf..6f70515009 100644
--- a/modules/indexer/issues/elasticsearch/elasticsearch.go
+++ b/modules/indexer/issues/elasticsearch/elasticsearch.go
@@ -197,8 +197,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	if options.ProjectID.Has() {
 		query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
 	}
-	if options.ProjectBoardID.Has() {
-		query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value()))
+	if options.ProjectColumnID.Has() {
+		query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
 	}
 
 	if options.PosterID.Has() {
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index 0d0cfc8516..e426229f78 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -369,13 +369,13 @@ func searchIssueInProject(t *testing.T) {
 		},
 		{
 			SearchOptions{
-				ProjectBoardID: optional.Some(int64(1)),
+				ProjectColumnID: optional.Some(int64(1)),
 			},
 			[]int64{1},
 		},
 		{
 			SearchOptions{
-				ProjectBoardID: optional.Some(int64(0)), // issue with in default board
+				ProjectColumnID: optional.Some(int64(0)), // issue with in default column
 			},
 			[]int64{2},
 		},
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index e9c4eca559..2dfee8b72e 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -27,7 +27,7 @@ type IndexerData struct {
 	NoLabel            bool               `json:"no_label"` // True if LabelIDs is empty
 	MilestoneID        int64              `json:"milestone_id"`
 	ProjectID          int64              `json:"project_id"`
-	ProjectBoardID     int64              `json:"project_board_id"`
+	ProjectColumnID    int64              `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible
 	PosterID           int64              `json:"poster_id"`
 	AssigneeID         int64              `json:"assignee_id"`
 	MentionIDs         []int64            `json:"mention_ids"`
@@ -89,8 +89,8 @@ type SearchOptions struct {
 
 	MilestoneIDs []int64 // milestones the issues have
 
-	ProjectID      optional.Option[int64] // project the issues belong to
-	ProjectBoardID optional.Option[int64] // project board the issues belong to
+	ProjectID       optional.Option[int64] // project the issues belong to
+	ProjectColumnID optional.Option[int64] // project column the issues belong to
 
 	PosterID optional.Option[int64] // poster of the issues
 
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 7f32876d80..16f0a78ec0 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -338,38 +338,38 @@ var cases = []*testIndexerCase{
 		},
 	},
 	{
-		Name: "ProjectBoardID",
+		Name: "ProjectColumnID",
 		SearchOptions: &internal.SearchOptions{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectBoardID: optional.Some(int64(1)),
+			ProjectColumnID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
 			for _, v := range result.Hits {
-				assert.Equal(t, int64(1), data[v.ID].ProjectBoardID)
+				assert.Equal(t, int64(1), data[v.ID].ProjectColumnID)
 			}
 			assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
-				return v.ProjectBoardID == 1
+				return v.ProjectColumnID == 1
 			}), result.Total)
 		},
 	},
 	{
-		Name: "no ProjectBoardID",
+		Name: "no ProjectColumnID",
 		SearchOptions: &internal.SearchOptions{
 			Paginator: &db.ListOptions{
 				PageSize: 5,
 			},
-			ProjectBoardID: optional.Some(int64(0)),
+			ProjectColumnID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
 			assert.Equal(t, 5, len(result.Hits))
 			for _, v := range result.Hits {
-				assert.Equal(t, int64(0), data[v.ID].ProjectBoardID)
+				assert.Equal(t, int64(0), data[v.ID].ProjectColumnID)
 			}
 			assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
-				return v.ProjectBoardID == 0
+				return v.ProjectColumnID == 0
 			}), result.Total)
 		},
 	},
@@ -706,7 +706,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
 				NoLabel:            len(labelIDs) == 0,
 				MilestoneID:        issueIndex % 4,
 				ProjectID:          issueIndex % 5,
-				ProjectBoardID:     issueIndex % 6,
+				ProjectColumnID:    issueIndex % 6,
 				PosterID:           id%10 + 1, // PosterID should not be 0
 				AssigneeID:         issueIndex % 10,
 				MentionIDs:         mentionIDs,
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index 8a7cec6cba..9332319339 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -174,8 +174,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
 	if options.ProjectID.Has() {
 		query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
 	}
-	if options.ProjectBoardID.Has() {
-		query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value()))
+	if options.ProjectColumnID.Has() {
+		query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
 	}
 
 	if options.PosterID.Has() {
diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go
index 9861c808dc..e752ae6f24 100644
--- a/modules/indexer/issues/util.go
+++ b/modules/indexer/issues/util.go
@@ -105,7 +105,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
 		NoLabel:            len(labels) == 0,
 		MilestoneID:        issue.MilestoneID,
 		ProjectID:          projectID,
-		ProjectBoardID:     issue.ProjectBoardID(ctx),
+		ProjectColumnID:    issue.ProjectColumnID(ctx),
 		PosterID:           issue.PosterID,
 		AssigneeID:         issue.AssigneeID,
 		MentionIDs:         mentionIDs,
diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go
index 1bf8f58b93..230260ff94 100755
--- a/modules/metrics/collector.go
+++ b/modules/metrics/collector.go
@@ -36,7 +36,7 @@ type Collector struct {
 	Oauths             *prometheus.Desc
 	Organizations      *prometheus.Desc
 	Projects           *prometheus.Desc
-	ProjectBoards      *prometheus.Desc
+	ProjectColumns     *prometheus.Desc
 	PublicKeys         *prometheus.Desc
 	Releases           *prometheus.Desc
 	Repositories       *prometheus.Desc
@@ -146,9 +146,9 @@ func NewCollector() Collector {
 			"Number of projects",
 			nil, nil,
 		),
-		ProjectBoards: prometheus.NewDesc(
-			namespace+"projects_boards",
-			"Number of project boards",
+		ProjectColumns: prometheus.NewDesc(
+			namespace+"projects_boards", // TODO: change the key name will affect the consume's result history
+			"Number of project columns",
 			nil, nil,
 		),
 		PublicKeys: prometheus.NewDesc(
@@ -219,7 +219,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
 	ch <- c.Oauths
 	ch <- c.Organizations
 	ch <- c.Projects
-	ch <- c.ProjectBoards
+	ch <- c.ProjectColumns
 	ch <- c.PublicKeys
 	ch <- c.Releases
 	ch <- c.Repositories
@@ -336,9 +336,9 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
 		float64(stats.Counter.Project),
 	)
 	ch <- prometheus.MustNewConstMetric(
-		c.ProjectBoards,
+		c.ProjectColumns,
 		prometheus.GaugeValue,
-		float64(stats.Counter.ProjectBoard),
+		float64(stats.Counter.ProjectColumn),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		c.PublicKeys,
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 40cbdb23fe..fd47974fe9 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1215,7 +1215,7 @@ branches = Branches
 tags = Tags
 issues = Issues
 pulls = Pull Requests
-project_board = Projects
+projects = Projects
 packages = Packages
 actions = Actions
 labels = Labels
@@ -1379,7 +1379,7 @@ ext_issues = Access to External Issues
 ext_issues.desc = Link to an external issue tracker.
 
 projects = Projects
-projects.desc = Manage issues and pulls in project boards.
+projects.desc = Manage issues and pulls in projects.
 projects.description = Description (optional)
 projects.description_placeholder = Description
 projects.create = Create Project
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 50effbe963..8fb8f2540f 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -34,7 +34,7 @@ const (
 // MustEnableProjects check if projects are enabled in settings
 func MustEnableProjects(ctx *context.Context) {
 	if unit.TypeProjects.UnitGlobalDisabled() {
-		ctx.NotFound("EnableKanbanBoard", nil)
+		ctx.NotFound("EnableProjects", nil)
 		return
 	}
 }
@@ -42,7 +42,7 @@ func MustEnableProjects(ctx *context.Context) {
 // Projects renders the home page of projects
 func Projects(ctx *context.Context) {
 	shared_user.PrepareContextForProfileBigAvatar(ctx)
-	ctx.Data["Title"] = ctx.Tr("repo.project_board")
+	ctx.Data["Title"] = ctx.Tr("repo.projects")
 
 	sortType := ctx.FormTrim("sort")
 
@@ -139,7 +139,7 @@ func canWriteProjects(ctx *context.Context) bool {
 // RenderNewProject render creating a project page
 func RenderNewProject(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.projects.new")
-	ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
+	ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
 	ctx.Data["CardTypes"] = project_model.GetCardConfig()
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
 	ctx.Data["PageIsViewProjects"] = true
@@ -168,12 +168,12 @@ func NewProjectPost(ctx *context.Context) {
 	}
 
 	newProject := project_model.Project{
-		OwnerID:     ctx.ContextUser.ID,
-		Title:       form.Title,
-		Description: form.Content,
-		CreatorID:   ctx.Doer.ID,
-		BoardType:   form.BoardType,
-		CardType:    form.CardType,
+		OwnerID:      ctx.ContextUser.ID,
+		Title:        form.Title,
+		Description:  form.Content,
+		CreatorID:    ctx.Doer.ID,
+		TemplateType: form.TemplateType,
+		CardType:     form.CardType,
 	}
 
 	if ctx.ContextUser.IsOrganization() {
@@ -314,7 +314,7 @@ func EditProjectPost(ctx *context.Context) {
 	}
 }
 
-// ViewProject renders the project board for a project
+// ViewProject renders the project with board view for a project
 func ViewProject(ctx *context.Context) {
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
@@ -326,15 +326,15 @@ func ViewProject(ctx *context.Context) {
 		return
 	}
 
-	boards, err := project.GetBoards(ctx)
+	columns, err := project.GetColumns(ctx)
 	if err != nil {
-		ctx.ServerError("GetProjectBoards", err)
+		ctx.ServerError("GetProjectColumns", err)
 		return
 	}
 
-	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
+	issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
 	if err != nil {
-		ctx.ServerError("LoadIssuesOfBoards", err)
+		ctx.ServerError("LoadIssuesOfColumns", err)
 		return
 	}
 
@@ -377,7 +377,7 @@ func ViewProject(ctx *context.Context) {
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
 	ctx.Data["Project"] = project
 	ctx.Data["IssuesMap"] = issuesMap
-	ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend
+	ctx.Data["Columns"] = columns
 	shared_user.RenderUserHeader(ctx)
 
 	err = shared_user.LoadHeaderCount(ctx)
@@ -389,8 +389,8 @@ func ViewProject(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplProjectsView)
 }
 
-// DeleteProjectBoard allows for the deletion of a project board
-func DeleteProjectBoard(ctx *context.Context) {
+// DeleteProjectColumn allows for the deletion of a project column
+func DeleteProjectColumn(ctx *context.Context) {
 	if ctx.Doer == nil {
 		ctx.JSON(http.StatusForbidden, map[string]string{
 			"message": "Only signed in users are allowed to perform this action.",
@@ -404,36 +404,36 @@ func DeleteProjectBoard(ctx *context.Context) {
 		return
 	}
 
-	pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
 	if err != nil {
-		ctx.ServerError("GetProjectBoard", err)
+		ctx.ServerError("GetProjectColumn", err)
 		return
 	}
 	if pb.ProjectID != ctx.ParamsInt64(":id") {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
 		})
 		return
 	}
 
 	if project.OwnerID != ctx.ContextUser.ID {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
 		})
 		return
 	}
 
-	if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil {
-		ctx.ServerError("DeleteProjectBoardByID", err)
+	if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
+		ctx.ServerError("DeleteProjectColumnByID", err)
 		return
 	}
 
 	ctx.JSONOK()
 }
 
-// AddBoardToProjectPost allows a new board to be added to a project.
-func AddBoardToProjectPost(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+// AddColumnToProjectPost allows a new column to be added to a project.
+func AddColumnToProjectPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
 
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
@@ -441,21 +441,21 @@ func AddBoardToProjectPost(ctx *context.Context) {
 		return
 	}
 
-	if err := project_model.NewBoard(ctx, &project_model.Board{
+	if err := project_model.NewColumn(ctx, &project_model.Column{
 		ProjectID: project.ID,
 		Title:     form.Title,
 		Color:     form.Color,
 		CreatorID: ctx.Doer.ID,
 	}); err != nil {
-		ctx.ServerError("NewProjectBoard", err)
+		ctx.ServerError("NewProjectColumn", err)
 		return
 	}
 
 	ctx.JSONOK()
 }
 
-// CheckProjectBoardChangePermissions check permission
-func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
+// CheckProjectColumnChangePermissions check permission
+func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
 	if ctx.Doer == nil {
 		ctx.JSON(http.StatusForbidden, map[string]string{
 			"message": "Only signed in users are allowed to perform this action.",
@@ -469,62 +469,60 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
 		return nil, nil
 	}
 
-	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
 	if err != nil {
-		ctx.ServerError("GetProjectBoard", err)
+		ctx.ServerError("GetProjectColumn", err)
 		return nil, nil
 	}
-	if board.ProjectID != ctx.ParamsInt64(":id") {
+	if column.ProjectID != ctx.ParamsInt64(":id") {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
 		})
 		return nil, nil
 	}
 
 	if project.OwnerID != ctx.ContextUser.ID {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID),
 		})
 		return nil, nil
 	}
-	return project, board
+	return project, column
 }
 
-// EditProjectBoard allows a project board's to be updated
-func EditProjectBoard(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
-	_, board := CheckProjectBoardChangePermissions(ctx)
+// EditProjectColumn allows a project column's to be updated
+func EditProjectColumn(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
+	_, column := CheckProjectColumnChangePermissions(ctx)
 	if ctx.Written() {
 		return
 	}
 
 	if form.Title != "" {
-		board.Title = form.Title
+		column.Title = form.Title
 	}
-
-	board.Color = form.Color
-
+	column.Color = form.Color
 	if form.Sorting != 0 {
-		board.Sorting = form.Sorting
+		column.Sorting = form.Sorting
 	}
 
-	if err := project_model.UpdateBoard(ctx, board); err != nil {
-		ctx.ServerError("UpdateProjectBoard", err)
+	if err := project_model.UpdateColumn(ctx, column); err != nil {
+		ctx.ServerError("UpdateProjectColumn", err)
 		return
 	}
 
 	ctx.JSONOK()
 }
 
-// SetDefaultProjectBoard set default board for uncategorized issues/pulls
-func SetDefaultProjectBoard(ctx *context.Context) {
-	project, board := CheckProjectBoardChangePermissions(ctx)
+// SetDefaultProjectColumn set default column for uncategorized issues/pulls
+func SetDefaultProjectColumn(ctx *context.Context) {
+	project, column := CheckProjectColumnChangePermissions(ctx)
 	if ctx.Written() {
 		return
 	}
 
-	if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil {
-		ctx.ServerError("SetDefaultBoard", err)
+	if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
+		ctx.ServerError("SetDefaultColumn", err)
 		return
 	}
 
@@ -550,14 +548,14 @@ func MoveIssues(ctx *context.Context) {
 		return
 	}
 
-	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
 	if err != nil {
-		ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
+		ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err)
 		return
 	}
 
-	if board.ProjectID != project.ID {
-		ctx.NotFound("BoardNotInProject", nil)
+	if column.ProjectID != project.ID {
+		ctx.NotFound("ColumnNotInProject", nil)
 		return
 	}
 
@@ -602,8 +600,8 @@ func MoveIssues(ctx *context.Context) {
 		}
 	}
 
-	if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
-		ctx.ServerError("MoveIssuesOnProjectBoard", err)
+	if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
+		ctx.ServerError("MoveIssuesOnProjectColumn", err)
 		return
 	}
 
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
index f4ccfe1c06..ab419cc878 100644
--- a/routers/web/org/projects_test.go
+++ b/routers/web/org/projects_test.go
@@ -13,16 +13,16 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestCheckProjectBoardChangePermissions(t *testing.T) {
+func TestCheckProjectColumnChangePermissions(t *testing.T) {
 	unittest.PrepareTestEnv(t)
 	ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4")
 	contexttest.LoadUser(t, ctx, 2)
 	ctx.ContextUser = ctx.Doer // user2
 	ctx.SetParams(":id", "4")
-	ctx.SetParams(":boardID", "4")
+	ctx.SetParams(":columnID", "4")
 
-	project, board := org.CheckProjectBoardChangePermissions(ctx)
+	project, column := org.CheckProjectColumnChangePermissions(ctx)
 	assert.NotNil(t, project)
-	assert.NotNil(t, board)
+	assert.NotNil(t, column)
 	assert.False(t, ctx.Written())
 }
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 0c8363a168..465dafefd3 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -2826,12 +2826,12 @@ func ListIssues(ctx *context.Context) {
 			Page:     ctx.FormInt("page"),
 			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 		},
-		Keyword:        keyword,
-		RepoIDs:        []int64{ctx.Repo.Repository.ID},
-		IsPull:         isPull,
-		IsClosed:       isClosed,
-		ProjectBoardID: projectID,
-		SortBy:         issue_indexer.SortByCreatedDesc,
+		Keyword:   keyword,
+		RepoIDs:   []int64{ctx.Repo.Repository.ID},
+		IsPull:    isPull,
+		IsClosed:  isClosed,
+		ProjectID: projectID,
+		SortBy:    issue_indexer.SortByCreatedDesc,
 	}
 	if since != 0 {
 		searchOpt.UpdatedAfterUnix = optional.Some(since)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 6186ee150c..9ce5535a0e 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -36,7 +36,7 @@ const (
 // MustEnableRepoProjects check if repo projects are enabled in settings
 func MustEnableRepoProjects(ctx *context.Context) {
 	if unit.TypeProjects.UnitGlobalDisabled() {
-		ctx.NotFound("EnableKanbanBoard", nil)
+		ctx.NotFound("EnableRepoProjects", nil)
 		return
 	}
 
@@ -51,7 +51,7 @@ func MustEnableRepoProjects(ctx *context.Context) {
 
 // Projects renders the home page of projects
 func Projects(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("repo.project_board")
+	ctx.Data["Title"] = ctx.Tr("repo.projects")
 
 	sortType := ctx.FormTrim("sort")
 
@@ -132,7 +132,7 @@ func Projects(ctx *context.Context) {
 // RenderNewProject render creating a project page
 func RenderNewProject(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.projects.new")
-	ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
+	ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
 	ctx.Data["CardTypes"] = project_model.GetCardConfig()
 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
 	ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects"
@@ -150,13 +150,13 @@ func NewProjectPost(ctx *context.Context) {
 	}
 
 	if err := project_model.NewProject(ctx, &project_model.Project{
-		RepoID:      ctx.Repo.Repository.ID,
-		Title:       form.Title,
-		Description: form.Content,
-		CreatorID:   ctx.Doer.ID,
-		BoardType:   form.BoardType,
-		CardType:    form.CardType,
-		Type:        project_model.TypeRepository,
+		RepoID:       ctx.Repo.Repository.ID,
+		Title:        form.Title,
+		Description:  form.Content,
+		CreatorID:    ctx.Doer.ID,
+		TemplateType: form.TemplateType,
+		CardType:     form.CardType,
+		Type:         project_model.TypeRepository,
 	}); err != nil {
 		ctx.ServerError("NewProject", err)
 		return
@@ -289,7 +289,7 @@ func EditProjectPost(ctx *context.Context) {
 	}
 }
 
-// ViewProject renders the project board for a project
+// ViewProject renders the project with board view
 func ViewProject(ctx *context.Context) {
 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
 	if err != nil {
@@ -305,15 +305,15 @@ func ViewProject(ctx *context.Context) {
 		return
 	}
 
-	boards, err := project.GetBoards(ctx)
+	columns, err := project.GetColumns(ctx)
 	if err != nil {
-		ctx.ServerError("GetProjectBoards", err)
+		ctx.ServerError("GetProjectColumns", err)
 		return
 	}
 
-	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
+	issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
 	if err != nil {
-		ctx.ServerError("LoadIssuesOfBoards", err)
+		ctx.ServerError("LoadIssuesOfColumns", err)
 		return
 	}
 
@@ -368,7 +368,7 @@ func ViewProject(ctx *context.Context) {
 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
 	ctx.Data["Project"] = project
 	ctx.Data["IssuesMap"] = issuesMap
-	ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend
+	ctx.Data["Columns"] = columns
 
 	ctx.HTML(http.StatusOK, tplProjectsView)
 }
@@ -406,8 +406,8 @@ func UpdateIssueProject(ctx *context.Context) {
 	ctx.JSONOK()
 }
 
-// DeleteProjectBoard allows for the deletion of a project board
-func DeleteProjectBoard(ctx *context.Context) {
+// DeleteProjectColumn allows for the deletion of a project column
+func DeleteProjectColumn(ctx *context.Context) {
 	if ctx.Doer == nil {
 		ctx.JSON(http.StatusForbidden, map[string]string{
 			"message": "Only signed in users are allowed to perform this action.",
@@ -432,36 +432,36 @@ func DeleteProjectBoard(ctx *context.Context) {
 		return
 	}
 
-	pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
 	if err != nil {
-		ctx.ServerError("GetProjectBoard", err)
+		ctx.ServerError("GetProjectColumn", err)
 		return
 	}
 	if pb.ProjectID != ctx.ParamsInt64(":id") {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
 		})
 		return
 	}
 
 	if project.RepoID != ctx.Repo.Repository.ID {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
 		})
 		return
 	}
 
-	if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil {
-		ctx.ServerError("DeleteProjectBoardByID", err)
+	if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
+		ctx.ServerError("DeleteProjectColumnByID", err)
 		return
 	}
 
 	ctx.JSONOK()
 }
 
-// AddBoardToProjectPost allows a new board to be added to a project.
-func AddBoardToProjectPost(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
+// AddColumnToProjectPost allows a new column to be added to a project.
+func AddColumnToProjectPost(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
 	if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) {
 		ctx.JSON(http.StatusForbidden, map[string]string{
 			"message": "Only authorized users are allowed to perform this action.",
@@ -479,20 +479,20 @@ func AddBoardToProjectPost(ctx *context.Context) {
 		return
 	}
 
-	if err := project_model.NewBoard(ctx, &project_model.Board{
+	if err := project_model.NewColumn(ctx, &project_model.Column{
 		ProjectID: project.ID,
 		Title:     form.Title,
 		Color:     form.Color,
 		CreatorID: ctx.Doer.ID,
 	}); err != nil {
-		ctx.ServerError("NewProjectBoard", err)
+		ctx.ServerError("NewProjectColumn", err)
 		return
 	}
 
 	ctx.JSONOK()
 }
 
-func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
+func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
 	if ctx.Doer == nil {
 		ctx.JSON(http.StatusForbidden, map[string]string{
 			"message": "Only signed in users are allowed to perform this action.",
@@ -517,62 +517,60 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
 		return nil, nil
 	}
 
-	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
 	if err != nil {
-		ctx.ServerError("GetProjectBoard", err)
+		ctx.ServerError("GetProjectColumn", err)
 		return nil, nil
 	}
-	if board.ProjectID != ctx.ParamsInt64(":id") {
+	if column.ProjectID != ctx.ParamsInt64(":id") {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
 		})
 		return nil, nil
 	}
 
 	if project.RepoID != ctx.Repo.Repository.ID {
 		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
-			"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
+			"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, ctx.Repo.Repository.ID),
 		})
 		return nil, nil
 	}
-	return project, board
+	return project, column
 }
 
-// EditProjectBoard allows a project board's to be updated
-func EditProjectBoard(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
-	_, board := checkProjectBoardChangePermissions(ctx)
+// EditProjectColumn allows a project column's to be updated
+func EditProjectColumn(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
+	_, column := checkProjectColumnChangePermissions(ctx)
 	if ctx.Written() {
 		return
 	}
 
 	if form.Title != "" {
-		board.Title = form.Title
+		column.Title = form.Title
 	}
-
-	board.Color = form.Color
-
+	column.Color = form.Color
 	if form.Sorting != 0 {
-		board.Sorting = form.Sorting
+		column.Sorting = form.Sorting
 	}
 
-	if err := project_model.UpdateBoard(ctx, board); err != nil {
-		ctx.ServerError("UpdateProjectBoard", err)
+	if err := project_model.UpdateColumn(ctx, column); err != nil {
+		ctx.ServerError("UpdateProjectColumn", err)
 		return
 	}
 
 	ctx.JSONOK()
 }
 
-// SetDefaultProjectBoard set default board for uncategorized issues/pulls
-func SetDefaultProjectBoard(ctx *context.Context) {
-	project, board := checkProjectBoardChangePermissions(ctx)
+// SetDefaultProjectColumn set default column for uncategorized issues/pulls
+func SetDefaultProjectColumn(ctx *context.Context) {
+	project, column := checkProjectColumnChangePermissions(ctx)
 	if ctx.Written() {
 		return
 	}
 
-	if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil {
-		ctx.ServerError("SetDefaultBoard", err)
+	if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
+		ctx.ServerError("SetDefaultColumn", err)
 		return
 	}
 
@@ -609,18 +607,18 @@ func MoveIssues(ctx *context.Context) {
 		return
 	}
 
-	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+	column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
 	if err != nil {
-		if project_model.IsErrProjectBoardNotExist(err) {
-			ctx.NotFound("ProjectBoardNotExist", nil)
+		if project_model.IsErrProjectColumnNotExist(err) {
+			ctx.NotFound("ProjectColumnNotExist", nil)
 		} else {
-			ctx.ServerError("GetProjectBoard", err)
+			ctx.ServerError("GetProjectColumn", err)
 		}
 		return
 	}
 
-	if board.ProjectID != project.ID {
-		ctx.NotFound("BoardNotInProject", nil)
+	if column.ProjectID != project.ID {
+		ctx.NotFound("ColumnNotInProject", nil)
 		return
 	}
 
@@ -664,8 +662,8 @@ func MoveIssues(ctx *context.Context) {
 		}
 	}
 
-	if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
-		ctx.ServerError("MoveIssuesOnProjectBoard", err)
+	if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
+		ctx.ServerError("MoveIssuesOnProjectColumn", err)
 		return
 	}
 
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
index 479f8c55a2..d61230a57e 100644
--- a/routers/web/repo/projects_test.go
+++ b/routers/web/repo/projects_test.go
@@ -12,16 +12,16 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestCheckProjectBoardChangePermissions(t *testing.T) {
+func TestCheckProjectColumnChangePermissions(t *testing.T) {
 	unittest.PrepareTestEnv(t)
 	ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2")
 	contexttest.LoadUser(t, ctx, 2)
 	contexttest.LoadRepo(t, ctx, 1)
 	ctx.SetParams(":id", "1")
-	ctx.SetParams(":boardID", "2")
+	ctx.SetParams(":columnID", "2")
 
-	project, board := checkProjectBoardChangePermissions(ctx)
+	project, column := checkProjectColumnChangePermissions(ctx)
 	assert.NotNil(t, project)
-	assert.NotNil(t, board)
+	assert.NotNil(t, column)
 	assert.False(t, ctx.Written())
 }
diff --git a/routers/web/web.go b/routers/web/web.go
index 194a67bf03..6a17c19821 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1000,7 +1000,7 @@ func registerRoutes(m *web.Route) {
 				m.Get("/new", org.RenderNewProject)
 				m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
 				m.Group("/{id}", func() {
-					m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
+					m.Post("", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost)
 					m.Post("/move", project.MoveColumns)
 					m.Post("/delete", org.DeleteProject)
 
@@ -1008,10 +1008,10 @@ func registerRoutes(m *web.Route) {
 					m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
 					m.Post("/{action:open|close}", org.ChangeProjectStatus)
 
-					m.Group("/{boardID}", func() {
-						m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
-						m.Delete("", org.DeleteProjectBoard)
-						m.Post("/default", org.SetDefaultProjectBoard)
+					m.Group("/{columnID}", func() {
+						m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn)
+						m.Delete("", org.DeleteProjectColumn)
+						m.Post("/default", org.SetDefaultProjectColumn)
 						m.Post("/move", org.MoveIssues)
 					})
 				})
@@ -1356,7 +1356,7 @@ func registerRoutes(m *web.Route) {
 			m.Get("/new", repo.RenderNewProject)
 			m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
 			m.Group("/{id}", func() {
-				m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
+				m.Post("", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost)
 				m.Post("/move", project.MoveColumns)
 				m.Post("/delete", repo.DeleteProject)
 
@@ -1364,10 +1364,10 @@ func registerRoutes(m *web.Route) {
 				m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
 				m.Post("/{action:open|close}", repo.ChangeProjectStatus)
 
-				m.Group("/{boardID}", func() {
-					m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
-					m.Delete("", repo.DeleteProjectBoard)
-					m.Post("/default", repo.SetDefaultProjectBoard)
+				m.Group("/{columnID}", func() {
+					m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn)
+					m.Delete("", repo.DeleteProjectColumn)
+					m.Post("/default", repo.SetDefaultProjectColumn)
 					m.Post("/move", repo.MoveIssues)
 				})
 			})
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index f49cc2e86b..32d96abf4d 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -505,45 +505,21 @@ func (i IssueLockForm) HasValidReason() bool {
 	return false
 }
 
-// __________                   __               __
-// \______   \_______  ____    |__| ____   _____/  |_  ______
-//  |     ___/\_  __ \/  _ \   |  |/ __ \_/ ___\   __\/  ___/
-//  |    |     |  | \(  <_> )  |  \  ___/\  \___|  |  \___ \
-//  |____|     |__|   \____/\__|  |\___  >\___  >__| /____  >
-//                         \______|    \/     \/          \/
-
 // CreateProjectForm form for creating a project
 type CreateProjectForm struct {
-	Title     string `binding:"Required;MaxSize(100)"`
-	Content   string
-	BoardType project_model.BoardType
-	CardType  project_model.CardType
+	Title        string `binding:"Required;MaxSize(100)"`
+	Content      string
+	TemplateType project_model.TemplateType
+	CardType     project_model.CardType
 }
 
-// UserCreateProjectForm is a from for creating an individual or organization
-// form.
-type UserCreateProjectForm struct {
-	Title     string `binding:"Required;MaxSize(100)"`
-	Content   string
-	BoardType project_model.BoardType
-	CardType  project_model.CardType
-	UID       int64 `binding:"Required"`
-}
-
-// EditProjectBoardForm is a form for editing a project board
-type EditProjectBoardForm struct {
+// EditProjectColumnForm is a form for editing a project column
+type EditProjectColumnForm struct {
 	Title   string `binding:"Required;MaxSize(100)"`
 	Sorting int8
 	Color   string `binding:"MaxSize(7)"`
 }
 
-//    _____  .__.__                   __
-//   /     \ |__|  |   ____   _______/  |_  ____   ____   ____
-//  /  \ /  \|  |  | _/ __ \ /  ___/\   __\/  _ \ /    \_/ __ \
-// /    Y    \  |  |_\  ___/ \___ \  |  | (  <_> )   |  \  ___/
-// \____|__  /__|____/\___  >____  > |__|  \____/|___|  /\___  >
-//         \/             \/     \/                   \/     \/
-
 // CreateMilestoneForm form for creating milestone
 type CreateMilestoneForm struct {
 	Title    string `binding:"Required;MaxSize(50)"`
@@ -557,13 +533,6 @@ func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) b
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 }
 
-// .____          ___.          .__
-// |    |   _____ \_ |__   ____ |  |
-// |    |   \__  \ | __ \_/ __ \|  |
-// |    |___ / __ \| \_\ \  ___/|  |__
-// |_______ (____  /___  /\___  >____/
-//         \/    \/    \/     \/
-
 // CreateLabelForm form for creating label
 type CreateLabelForm struct {
 	ID          int64
@@ -591,13 +560,6 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
 	return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
 }
 
-// __________      .__  .__    __________                                     __
-// \______   \__ __|  | |  |   \______   \ ____  ________ __   ____   _______/  |_
-//  |     ___/  |  \  | |  |    |       _// __ \/ ____/  |  \_/ __ \ /  ___/\   __\
-//  |    |   |  |  /  |_|  |__  |    |   \  ___< <_|  |  |  /\  ___/ \___ \  |  |
-//  |____|   |____/|____/____/  |____|_  /\___  >__   |____/  \___  >____  > |__|
-//                                     \/     \/   |__|           \/     \/
-
 // MergePullRequestForm form for merging Pull Request
 // swagger:model MergePullRequestOption
 type MergePullRequestForm struct {
diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go
index c21fddf478..b9677c1800 100644
--- a/services/forms/user_form_hidden_comments.go
+++ b/services/forms/user_form_hidden_comments.go
@@ -65,7 +65,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
 	},
 	"project": {
 		/*30*/ issues_model.CommentTypeProject,
-		/*31*/ issues_model.CommentTypeProjectBoard,
+		/*31*/ issues_model.CommentTypeProjectColumn,
 	},
 	"issue_ref": {
 		/*33*/ issues_model.CommentTypeChangeIssueRef,
diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl
index 92ee36c1c4..bd173b54bc 100644
--- a/templates/projects/new.tmpl
+++ b/templates/projects/new.tmpl
@@ -25,11 +25,11 @@
 			<div class="field">
 				<label>{{ctx.Locale.Tr "repo.projects.template.desc"}}</label>
 				<div class="ui selection dropdown">
-					<input type="hidden" name="board_type" value="{{.type}}">
+					<input type="hidden" name="template_type" value="{{.type}}">
 					<div class="default text">{{ctx.Locale.Tr "repo.projects.template.desc_helper"}}</div>
 					<div class="menu">
-						{{range $element := .BoardTypes}}
-							<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{ctx.Locale.Tr $element.Translation}}</div>
+						{{range $element := .TemplateConfigs}}
+							<div class="item" data-id="{{$element.TemplateType}}" data-value="{{$element.TemplateType}}">{{ctx.Locale.Tr $element.Translation}}</div>
 						{{end}}
 					</div>
 				</div>
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 34f47b7d89..22daaab4bc 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -180,7 +180,7 @@
 					{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}}
 					{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
 						<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
-							{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}}
+							{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.projects"}}
 							{{if .Repository.NumOpenProjects}}
 								<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span>
 							{{end}}
diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl
index f23ca36d78..18986db773 100644
--- a/templates/repo/issue/filter_actions.tmpl
+++ b/templates/repo/issue/filter_actions.tmpl
@@ -71,7 +71,7 @@
 		<!-- Projects -->
 		<div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item">
 			<span class="text">
-				{{ctx.Locale.Tr "repo.project_board"}}
+				{{ctx.Locale.Tr "repo.projects"}}
 			</span>
 			{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 			<div class="menu">
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 3168384072..6c49f00094 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -467,7 +467,7 @@
 				{{$isProjectsGlobalDisabled := ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled}}
 				{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}}
 				<div class="inline field">
-					<label>{{ctx.Locale.Tr "repo.project_board"}}</label>
+					<label>{{ctx.Locale.Tr "repo.projects"}}</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" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
 						<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
diff --git a/tests/integration/project_test.go b/tests/integration/project_test.go
index 1d9c3aae53..cdff9aa2fd 100644
--- a/tests/integration/project_test.go
+++ b/tests/integration/project_test.go
@@ -39,23 +39,23 @@ func TestMoveRepoProjectColumns(t *testing.T) {
 	assert.True(t, projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo))
 
 	project1 := project_model.Project{
-		Title:     "new created project",
-		RepoID:    repo2.ID,
-		Type:      project_model.TypeRepository,
-		BoardType: project_model.BoardTypeNone,
+		Title:        "new created project",
+		RepoID:       repo2.ID,
+		Type:         project_model.TypeRepository,
+		TemplateType: project_model.TemplateTypeNone,
 	}
 	err := project_model.NewProject(db.DefaultContext, &project1)
 	assert.NoError(t, err)
 
 	for i := 0; i < 3; i++ {
-		err = project_model.NewBoard(db.DefaultContext, &project_model.Board{
+		err = project_model.NewColumn(db.DefaultContext, &project_model.Column{
 			Title:     fmt.Sprintf("column %d", i+1),
 			ProjectID: project1.ID,
 		})
 		assert.NoError(t, err)
 	}
 
-	columns, err := project1.GetBoards(db.DefaultContext)
+	columns, err := project1.GetColumns(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Len(t, columns, 3)
 	assert.EqualValues(t, 0, columns[0].Sorting)
@@ -76,7 +76,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
 	})
 	sess.MakeRequest(t, req, http.StatusOK)
 
-	columnsAfter, err := project1.GetBoards(db.DefaultContext)
+	columnsAfter, err := project1.GetColumns(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Len(t, columns, 3)
 	assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index 21e2aee0a2..e25182051a 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -7,7 +7,7 @@
 }
 
 .project-column {
-  background-color: var(--color-project-board-bg) !important;
+  background-color: var(--color-project-column-bg) !important;
   border: 1px solid var(--color-secondary) !important;
   margin: 0 0.5rem !important;
   padding: 0.5rem !important;
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index ad9ab5a8c2..45102b64f5 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -216,7 +216,7 @@
   --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-project-column-bg: var(--color-secondary-light-2);
   --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
   --color-reaction-bg: #e8f3ff12;
   --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 8d4aa6df93..8c7fc8a00d 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -216,7 +216,7 @@
   --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);
+  --color-project-column-bg: var(--color-secondary-light-4);
   --color-caret: var(--color-text-dark);
   --color-reaction-bg: #0000170a;
   --color-reaction-hover-bg: var(--color-primary-light-5);

From c0880e7695346997c6a93f05cd01634cb3ad03ee Mon Sep 17 00:00:00 2001
From: Rowan Bohde <rowan.bohde@gmail.com>
Date: Mon, 27 May 2024 07:56:04 -0500
Subject: [PATCH 047/131] feat: add support for a credentials chain for minio
 access (#31051)

We wanted to be able to use the IAM role provided by the EC2 instance
metadata in order to access S3 via the Minio configuration. To do this,
a new credentials chain is added that will check the following locations
for credentials when an access key is not provided. In priority order,
they are:

1. MINIO_ prefixed environment variables
2. AWS_ prefixed environment variables
3. a minio credentials file
4. an aws credentials file
5. EC2 instance metadata
---
 custom/conf/app.example.ini                   |  10 +-
 .../config-cheat-sheet.en-us.md               |  18 ++-
 modules/storage/minio.go                      |  31 +++++-
 modules/storage/minio_test.go                 | 104 ++++++++++++++++++
 modules/storage/testdata/aws_credentials      |   3 +
 modules/storage/testdata/minio.json           |  12 ++
 6 files changed, 169 insertions(+), 9 deletions(-)
 create mode 100644 modules/storage/testdata/aws_credentials
 create mode 100644 modules/storage/testdata/minio.json

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index afbd20eb56..7c05e7fefd 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1872,7 +1872,10 @@ LEVEL = Info
 ;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 ;MINIO_ENDPOINT = localhost:9000
 ;;
-;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
+;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
+;; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
+;; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
+;; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 ;MINIO_ACCESS_KEY_ID =
 ;;
 ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
@@ -2573,7 +2576,10 @@ LEVEL = Info
 ;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 ;MINIO_ENDPOINT = localhost:9000
 ;;
-;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
+;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
+;; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
+;; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
+;; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 ;MINIO_ACCESS_KEY_ID =
 ;;
 ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 1165a83e25..2c15d161ea 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -843,7 +843,7 @@ Default templates for project board view:
 - `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
 - `PATH`: **attachments**: Path to store attachments only available when STORAGE_TYPE is `local`, relative paths will be resolved to `${AppDataPath}/${attachment.PATH}`.
 - `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when STORAGE_TYPE is `minio`
-- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
+- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 - `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 - `MINIO_BUCKET`: **gitea**: Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
 - `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when STORAGE_TYPE is `minio`
@@ -1274,7 +1274,7 @@ is `data/lfs` and the default of `MINIO_BASE_PATH` is `lfs/`.
 - `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
 - `PATH`: **./data/lfs**: Where to store LFS files, only available when `STORAGE_TYPE` is `local`. If not set it fall back to deprecated LFS_CONTENT_PATH value in [server] section.
 - `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
-- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
+- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 - `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
 - `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio`
 - `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
@@ -1290,7 +1290,7 @@ Default storage configuration for attachments, lfs, avatars, repo-avatars, repo-
 - `STORAGE_TYPE`: **local**: Storage type, `local` for local disk or `minio` for s3 compatible object storage service.
 - `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
 - `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
-- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
+- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 - `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
 - `MINIO_BUCKET`: **gitea**: Minio bucket to store the data only available when `STORAGE_TYPE` is `minio`
 - `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
@@ -1305,7 +1305,10 @@ The recommended storage configuration for minio like below:
 STORAGE_TYPE = minio
 ; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 MINIO_ENDPOINT = localhost:9000
-; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
+; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
+; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
+; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
+; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 MINIO_ACCESS_KEY_ID =
 ; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 MINIO_SECRET_ACCESS_KEY =
@@ -1354,7 +1357,10 @@ STORAGE_TYPE = my_minio
 STORAGE_TYPE = minio
 ; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
 MINIO_ENDPOINT = localhost:9000
-; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
+; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
+; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
+; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
+; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 MINIO_ACCESS_KEY_ID =
 ; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
 MINIO_SECRET_ACCESS_KEY =
@@ -1380,7 +1386,7 @@ is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`.
 - `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
 - `PATH`: **./data/repo-archive**: Where to store archive files, only available when `STORAGE_TYPE` is `local`.
 - `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
-- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
+- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
 - `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio`
 - `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio`
 - `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio`
diff --git a/modules/storage/minio.go b/modules/storage/minio.go
index 986332dfed..1b32b2f54f 100644
--- a/modules/storage/minio.go
+++ b/modules/storage/minio.go
@@ -97,7 +97,7 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
 	}
 
 	minioClient, err := minio.New(config.Endpoint, &minio.Options{
-		Creds:        credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
+		Creds:        buildMinioCredentials(config, credentials.DefaultIAMRoleEndpoint),
 		Secure:       config.UseSSL,
 		Transport:    &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
 		Region:       config.Location,
@@ -164,6 +164,35 @@ func (m *MinioStorage) buildMinioDirPrefix(p string) string {
 	return p
 }
 
+func buildMinioCredentials(config setting.MinioStorageConfig, iamEndpoint string) *credentials.Credentials {
+	// If static credentials are provided, use those
+	if config.AccessKeyID != "" {
+		return credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, "")
+	}
+
+	// Otherwise, fallback to a credentials chain for S3 access
+	chain := []credentials.Provider{
+		// configure based upon MINIO_ prefixed environment variables
+		&credentials.EnvMinio{},
+		// configure based upon AWS_ prefixed environment variables
+		&credentials.EnvAWS{},
+		// read credentials from MINIO_SHARED_CREDENTIALS_FILE
+		// environment variable, or default json config files
+		&credentials.FileMinioClient{},
+		// read credentials from AWS_SHARED_CREDENTIALS_FILE
+		// environment variable, or default credentials file
+		&credentials.FileAWSCredentials{},
+		// read IAM role from EC2 metadata endpoint if available
+		&credentials.IAM{
+			Endpoint: iamEndpoint,
+			Client: &http.Client{
+				Transport: http.DefaultTransport,
+			},
+		},
+	}
+	return credentials.NewChainCredentials(chain)
+}
+
 // Open opens a file
 func (m *MinioStorage) Open(path string) (Object, error) {
 	opts := minio.GetObjectOptions{}
diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go
index c6fbb91ab4..ad11046dd6 100644
--- a/modules/storage/minio_test.go
+++ b/modules/storage/minio_test.go
@@ -6,6 +6,7 @@ package storage
 import (
 	"context"
 	"net/http"
+	"net/http/httptest"
 	"os"
 	"testing"
 
@@ -92,3 +93,106 @@ func TestS3StorageBadRequest(t *testing.T) {
 	_, err := NewStorage(setting.MinioStorageType, cfg)
 	assert.ErrorContains(t, err, message)
 }
+
+func TestMinioCredentials(t *testing.T) {
+	const (
+		ExpectedAccessKey       = "ExampleAccessKeyID"
+		ExpectedSecretAccessKey = "ExampleSecretAccessKeyID"
+		// Use a FakeEndpoint for IAM credentials to avoid logging any
+		// potential real IAM credentials when running in EC2.
+		FakeEndpoint = "http://localhost"
+	)
+
+	t.Run("Static Credentials", func(t *testing.T) {
+		cfg := setting.MinioStorageConfig{
+			AccessKeyID:     ExpectedAccessKey,
+			SecretAccessKey: ExpectedSecretAccessKey,
+		}
+		creds := buildMinioCredentials(cfg, FakeEndpoint)
+		v, err := creds.Get()
+
+		assert.NoError(t, err)
+		assert.Equal(t, ExpectedAccessKey, v.AccessKeyID)
+		assert.Equal(t, ExpectedSecretAccessKey, v.SecretAccessKey)
+	})
+
+	t.Run("Chain", func(t *testing.T) {
+		cfg := setting.MinioStorageConfig{}
+
+		t.Run("EnvMinio", func(t *testing.T) {
+			t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
+			t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
+
+			creds := buildMinioCredentials(cfg, FakeEndpoint)
+			v, err := creds.Get()
+
+			assert.NoError(t, err)
+			assert.Equal(t, ExpectedAccessKey+"Minio", v.AccessKeyID)
+			assert.Equal(t, ExpectedSecretAccessKey+"Minio", v.SecretAccessKey)
+		})
+
+		t.Run("EnvAWS", func(t *testing.T) {
+			t.Setenv("AWS_ACCESS_KEY", ExpectedAccessKey+"AWS")
+			t.Setenv("AWS_SECRET_KEY", ExpectedSecretAccessKey+"AWS")
+
+			creds := buildMinioCredentials(cfg, FakeEndpoint)
+			v, err := creds.Get()
+
+			assert.NoError(t, err)
+			assert.Equal(t, ExpectedAccessKey+"AWS", v.AccessKeyID)
+			assert.Equal(t, ExpectedSecretAccessKey+"AWS", v.SecretAccessKey)
+		})
+
+		t.Run("FileMinio", func(t *testing.T) {
+			t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
+			// prevent loading any actual credentials files from the user
+			t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
+
+			creds := buildMinioCredentials(cfg, FakeEndpoint)
+			v, err := creds.Get()
+
+			assert.NoError(t, err)
+			assert.Equal(t, ExpectedAccessKey+"MinioFile", v.AccessKeyID)
+			assert.Equal(t, ExpectedSecretAccessKey+"MinioFile", v.SecretAccessKey)
+		})
+
+		t.Run("FileAWS", func(t *testing.T) {
+			// prevent loading any actual credentials files from the user
+			t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
+			t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/aws_credentials")
+
+			creds := buildMinioCredentials(cfg, FakeEndpoint)
+			v, err := creds.Get()
+
+			assert.NoError(t, err)
+			assert.Equal(t, ExpectedAccessKey+"AWSFile", v.AccessKeyID)
+			assert.Equal(t, ExpectedSecretAccessKey+"AWSFile", v.SecretAccessKey)
+		})
+
+		t.Run("IAM", func(t *testing.T) {
+			// prevent loading any actual credentials files from the user
+			t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
+			t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
+
+			// Spawn a server to emulate the EC2 Instance Metadata
+			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				// The client will actually make 3 requests here,
+				// first will be to get the IMDSv2 token, second to
+				// get the role, and third for the actual
+				// credentials. However, we can return credentials
+				// every request since we're not emulating a full
+				// IMDSv2 flow.
+				w.Write([]byte(`{"Code":"Success","AccessKeyId":"ExampleAccessKeyIDIAM","SecretAccessKey":"ExampleSecretAccessKeyIDIAM"}`))
+			}))
+			defer server.Close()
+
+			// Use the provided EC2 Instance Metadata server
+			creds := buildMinioCredentials(cfg, server.URL)
+			v, err := creds.Get()
+
+			assert.NoError(t, err)
+			assert.Equal(t, ExpectedAccessKey+"IAM", v.AccessKeyID)
+			assert.Equal(t, ExpectedSecretAccessKey+"IAM", v.SecretAccessKey)
+		})
+	})
+}
diff --git a/modules/storage/testdata/aws_credentials b/modules/storage/testdata/aws_credentials
new file mode 100644
index 0000000000..62a5488b51
--- /dev/null
+++ b/modules/storage/testdata/aws_credentials
@@ -0,0 +1,3 @@
+[default]
+aws_access_key_id=ExampleAccessKeyIDAWSFile
+aws_secret_access_key=ExampleSecretAccessKeyIDAWSFile
diff --git a/modules/storage/testdata/minio.json b/modules/storage/testdata/minio.json
new file mode 100644
index 0000000000..3876257626
--- /dev/null
+++ b/modules/storage/testdata/minio.json
@@ -0,0 +1,12 @@
+{
+        "version": "10",
+        "aliases": {
+                "s3": {
+                        "url": "https://s3.amazonaws.com",
+                        "accessKey": "ExampleAccessKeyIDMinioFile",
+                        "secretKey": "ExampleSecretAccessKeyIDMinioFile",
+                        "api": "S3v4",
+                        "path": "dns"
+                }
+        }
+}

From 20c40259f12d5c1f4547df10a627d888b473e1e4 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 27 May 2024 21:43:32 +0800
Subject: [PATCH 048/131] Fix missing memcache import (#31105)

Fix #31102
---
 modules/cache/cache.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/modules/cache/cache.go b/modules/cache/cache.go
index 2ca77bdb29..0753671158 100644
--- a/modules/cache/cache.go
+++ b/modules/cache/cache.go
@@ -8,6 +8,8 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/modules/setting"
+
+	_ "gitea.com/go-chi/cache/memcache" //nolint:depguard // memcache plugin for cache, it is required for config "ADAPTER=memcache"
 )
 
 var defaultCache StringCache

From 8fc2ec187290419252f2ade497655d62df3a1505 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 27 May 2024 21:53:33 +0800
Subject: [PATCH 049/131] Update pip related commands for docker (#31106)

Thanks to graelo and silverwind for figuring out the problem.

Fix #31101
---
 docs/content/administration/external-renderers.en-us.md | 6 ++----
 docs/content/administration/external-renderers.zh-cn.md | 6 ++----
 2 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/docs/content/administration/external-renderers.en-us.md b/docs/content/administration/external-renderers.en-us.md
index 1e41b80145..fec2ab64d4 100644
--- a/docs/content/administration/external-renderers.en-us.md
+++ b/docs/content/administration/external-renderers.en-us.md
@@ -38,12 +38,10 @@ FROM gitea/gitea:@version@
 COPY custom/app.ini /data/gitea/conf/app.ini
 [...]
 
-RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev py-pip python3-dev py3-pip py3-pyzmq
+RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev pandoc python3-dev py3-pyzmq pipx
 # install any other package you need for your external renderers
 
-RUN pip3 install --upgrade pip
-RUN pip3 install -U setuptools
-RUN pip3 install jupyter docutils
+RUN pipx install jupyter docutils --include-deps
 # add above any other python package you may need to install
 ```
 
diff --git a/docs/content/administration/external-renderers.zh-cn.md b/docs/content/administration/external-renderers.zh-cn.md
index fdf7315d7b..1e56d95a66 100644
--- a/docs/content/administration/external-renderers.zh-cn.md
+++ b/docs/content/administration/external-renderers.zh-cn.md
@@ -37,12 +37,10 @@ FROM gitea/gitea:@version@
 COPY custom/app.ini /data/gitea/conf/app.ini
 [...]
 
-RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev py-pip python3-dev py3-pip py3-pyzmq
+RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev pandoc python3-dev py3-pyzmq pipx
 # 安装其他您需要的外部渲染器的软件包
 
-RUN pip3 install --upgrade pip
-RUN pip3 install -U setuptools
-RUN pip3 install jupyter docutils
+RUN pipx install jupyter docutils --include-deps
 # 在上面添加您需要安装的任何其他 Python 软件包
 ```
 

From 89cc5011716850eb30c9e34130c4b2ecd9829252 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 27 May 2024 22:53:48 +0800
Subject: [PATCH 050/131] Move documents under actions (#31110)

Move secrets and badge under actions
---
 docs/content/usage/{ => actions}/badge.en-us.md   | 4 +---
 docs/content/usage/{ => actions}/secrets.en-us.md | 4 +---
 docs/content/usage/{ => actions}/secrets.zh-cn.md | 4 +---
 3 files changed, 3 insertions(+), 9 deletions(-)
 rename docs/content/usage/{ => actions}/badge.en-us.md (96%)
 rename docs/content/usage/{ => actions}/secrets.en-us.md (96%)
 rename docs/content/usage/{ => actions}/secrets.zh-cn.md (96%)

diff --git a/docs/content/usage/badge.en-us.md b/docs/content/usage/actions/badge.en-us.md
similarity index 96%
rename from docs/content/usage/badge.en-us.md
rename to docs/content/usage/actions/badge.en-us.md
index 212134e01c..de7a34f4e6 100644
--- a/docs/content/usage/badge.en-us.md
+++ b/docs/content/usage/actions/badge.en-us.md
@@ -5,11 +5,9 @@ slug: "badge"
 sidebar_position: 11
 toc: false
 draft: false
-aliases:
-  - /en-us/badge
 menu:
   sidebar:
-    parent: "usage"
+    parent: "actions"
     name: "Badge"
     sidebar_position: 11
     identifier: "Badge"
diff --git a/docs/content/usage/secrets.en-us.md b/docs/content/usage/actions/secrets.en-us.md
similarity index 96%
rename from docs/content/usage/secrets.en-us.md
rename to docs/content/usage/actions/secrets.en-us.md
index 8ad6746614..5bf1f1a1e8 100644
--- a/docs/content/usage/secrets.en-us.md
+++ b/docs/content/usage/actions/secrets.en-us.md
@@ -5,11 +5,9 @@ slug: "secrets"
 sidebar_position: 50
 draft: false
 toc: false
-aliases:
-  - /en-us/secrets
 menu:
   sidebar:
-    parent: "usage"
+    parent: "actions"
     name: "Secrets"
     sidebar_position: 50
     identifier: "usage-secrets"
diff --git a/docs/content/usage/secrets.zh-cn.md b/docs/content/usage/actions/secrets.zh-cn.md
similarity index 96%
rename from docs/content/usage/secrets.zh-cn.md
rename to docs/content/usage/actions/secrets.zh-cn.md
index 40e80dc785..939042f0a8 100644
--- a/docs/content/usage/secrets.zh-cn.md
+++ b/docs/content/usage/actions/secrets.zh-cn.md
@@ -5,11 +5,9 @@ slug: "secrets"
 sidebar_position: 50
 draft: false
 toc: false
-aliases:
-  - /zh-cn/secrets
 menu:
   sidebar:
-    parent: "usage"
+    parent: "actions"
     name: "密钥管理"
     sidebar_position: 50
     identifier: "usage-secrets"

From 1ed8e6aa5fad235506f211daa9dffd448d9d5ad4 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 27 May 2024 23:05:12 +0800
Subject: [PATCH 051/131] Update demo site location from try.gitea.io ->
 demo.gitea.com (#31054)

---
 .gitea/issue_template.md                                 | 4 ++--
 .github/ISSUE_TEMPLATE/bug-report.yaml                   | 4 ++--
 .github/ISSUE_TEMPLATE/ui.bug-report.yaml                | 2 +-
 CONTRIBUTING.md                                          | 4 ++--
 README.md                                                | 6 +++---
 README_ZH.md                                             | 2 +-
 docs/README.md                                           | 5 +----
 docs/README_ZH.md                                        | 6 +-----
 docs/content/development/api-usage.en-us.md              | 2 +-
 docs/content/help/faq.en-us.md                           | 6 +++---
 docs/content/help/faq.zh-cn.md                           | 6 +++---
 docs/content/help/support.en-us.md                       | 6 +++---
 docs/content/help/support.zh-cn.md                       | 6 +++---
 docs/content/index.en-us.md                              | 2 +-
 docs/content/usage/authentication.en-us.md               | 2 +-
 docs/content/usage/authentication.zh-cn.md               | 2 +-
 docs/content/usage/issue-pull-request-templates.en-us.md | 2 +-
 17 files changed, 30 insertions(+), 37 deletions(-)

diff --git a/.gitea/issue_template.md b/.gitea/issue_template.md
index 9ad186cca7..cf173a67ca 100644
--- a/.gitea/issue_template.md
+++ b/.gitea/issue_template.md
@@ -3,7 +3,7 @@
 <!--
     1. Please speak English, this is the language all maintainers can speak and write.
     2. Please ask questions or configuration/deploy problems on our Discord
-       server (https://discord.gg/gitea) or forum (https://discourse.gitea.io).
+       server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
     3. Please take a moment to check that your issue doesn't already exist.
     4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
     5. Please give all relevant information below for bug reports, because
@@ -21,7 +21,7 @@
   - [ ] MySQL
   - [ ] MSSQL
   - [ ] SQLite
-- Can you reproduce the bug at https://try.gitea.io:
+- Can you reproduce the bug at https://demo.gitea.com:
   - [ ] Yes (provide example URL)
   - [ ] No
 - Log gist:
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml
index 94c1bd0ab7..ed29bdb4e6 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yaml
@@ -37,7 +37,7 @@ body:
       label: Can you reproduce the bug on the Gitea demo site?
       description: |
         If so, please provide a URL in the Description field
-        URL of Gitea demo: https://try.gitea.io
+        URL of Gitea demo: https://demo.gitea.com
       options:
         - "Yes"
         - "No"
@@ -74,7 +74,7 @@ body:
     attributes:
       label: How are you running Gitea?
       description: |
-        Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package
+        Please include information on whether you built Gitea yourself, used one of our downloads, are using https://demo.gitea.com or are using some other package
         Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
         If you are using a package or systemd tell us what distribution you are using
     validations:
diff --git a/.github/ISSUE_TEMPLATE/ui.bug-report.yaml b/.github/ISSUE_TEMPLATE/ui.bug-report.yaml
index 387aee897b..1560879674 100644
--- a/.github/ISSUE_TEMPLATE/ui.bug-report.yaml
+++ b/.github/ISSUE_TEMPLATE/ui.bug-report.yaml
@@ -46,7 +46,7 @@ body:
       label: Can you reproduce the bug on the Gitea demo site?
       description: |
         If so, please provide a URL in the Description field
-        URL of Gitea demo: https://try.gitea.io
+        URL of Gitea demo: https://demo.gitea.com
       options:
         - "Yes"
         - "No"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5d20bc2589..04c06ffd14 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -77,7 +77,7 @@ If your issue has not been reported yet, [open an issue](https://github.com/go-g
 and answer the questions so we can understand and reproduce the problematic behavior. \
 Please write clear and concise instructions so that we can reproduce the behavior — even if it seems obvious. \
 The more detailed and specific you are, the faster we can fix the issue. \
-It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://try.gitea.io>, as perhaps your problem has already been fixed on a current version. \
+It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://demo.gitea.com>, as perhaps your problem has already been fixed on a current version. \
 Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report.
 
 Please be kind, remember that Gitea comes at no cost to you, and you're getting free help.
@@ -362,7 +362,7 @@ If you add a new feature or change an existing aspect of Gitea, the documentatio
 
 ## API v1
 
-The API is documented by [swagger](http://try.gitea.io/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
+The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
 
 ### GitHub API compatibility
 
diff --git a/README.md b/README.md
index f579449174..fd96f9efbd 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ This project has been
 [forked](https://blog.gitea.com/welcome-to-gitea/) from
 [Gogs](https://gogs.io) since November of 2016, but a lot has changed.
 
-For online demonstrations, you can visit [try.gitea.io](https://try.gitea.io).
+For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
 
 For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
 
@@ -56,7 +56,7 @@ 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).
+> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
 
 ## Contributing
 
@@ -80,7 +80,7 @@ https://docs.gitea.com/contributing/localization
 ## Further information
 
 For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.com/).
-If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create  a post in the [discourse forum](https://discourse.gitea.io/).
+If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create  a post in the [discourse forum](https://forum.gitea.com/).
 
 We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea).
 
diff --git a/README_ZH.md b/README_ZH.md
index 726c4273a6..7aa7900a47 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -18,7 +18,7 @@
 
 Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。
 
-如果你想试用在线演示,请访问 [try.gitea.io](https://try.gitea.io/)。
+如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。
 
 如果你想使用免费的 Gitea 服务(有仓库数量限制),请访问 [gitea.com](https://gitea.com/user/login)。
 
diff --git a/docs/README.md b/docs/README.md
index d9aa3b80b8..38958525ba 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,8 +1,5 @@
 # Gitea: Docs
 
-[![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea)
-[![](https://images.microbadger.com/badges/image/gitea/docs.svg)](http://microbadger.com/images/gitea/docs "Get your own image badge on microbadger.com")
-
 These docs are ingested by our [docs repo](https://gitea.com/gitea/gitea-docusaurus).
 
 ## Authors
@@ -18,5 +15,5 @@ for the full license text.
 ## Copyright
 
 ```
-Copyright (c) 2016 The Gitea Authors <https://gitea.io>
+Copyright (c) 2016 The Gitea Authors
 ```
diff --git a/docs/README_ZH.md b/docs/README_ZH.md
index 7d9003a8ab..deff4b5fc7 100644
--- a/docs/README_ZH.md
+++ b/docs/README_ZH.md
@@ -1,9 +1,5 @@
 # Gitea: 文档
 
-[![Build Status](http://drone.gitea.io/api/badges/go-gitea/docs/status.svg)](http://drone.gitea.io/go-gitea/docs)
-[![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea)
-[![](https://images.microbadger.com/badges/image/gitea/docs.svg)](http://microbadger.com/images/gitea/docs "Get your own image badge on microbadger.com")
-
 https://gitea.com/gitea/gitea-docusaurus
 
 ## 关于我们
@@ -18,5 +14,5 @@ https://gitea.com/gitea/gitea-docusaurus
 ## 版权声明
 
 ```
-Copyright (c) 2016 The Gitea Authors <https://gitea.io>
+Copyright (c) 2016 The Gitea Authors
 ```
diff --git a/docs/content/development/api-usage.en-us.md b/docs/content/development/api-usage.en-us.md
index 94dac70b88..4fe376b11b 100644
--- a/docs/content/development/api-usage.en-us.md
+++ b/docs/content/development/api-usage.en-us.md
@@ -117,7 +117,7 @@ curl -v "http://localhost/api/v1/repos/search?limit=1"
 API Reference guide is auto-generated by swagger and available on:
 `https://gitea.your.host/api/swagger`
 or on the
-[Gitea demo instance](https://try.gitea.io/api/swagger)
+[Gitea instance](https://gitea.com/api/swagger)
 
 The OpenAPI document is at:
 `https://gitea.your.host/swagger.v1.json`
diff --git a/docs/content/help/faq.en-us.md b/docs/content/help/faq.en-us.md
index ba39ec83b0..e94f342198 100644
--- a/docs/content/help/faq.en-us.md
+++ b/docs/content/help/faq.en-us.md
@@ -45,7 +45,7 @@ To migrate from GitHub to Gitea, you can use Gitea's built-in migration form.
 
 In order to migrate items such as issues, pull requests, etc. you will need to input at least your username.
 
-[Example (requires login)](https://try.gitea.io/repo/migrate)
+[Example (requires login)](https://demo.gitea.com/repo/migrate)
 
 To migrate from GitLab to Gitea, you can use this non-affiliated tool:
 
@@ -137,9 +137,9 @@ All Gitea instances have the built-in API and there is no way to disable it comp
 You can, however, disable showing its documentation by setting `ENABLE_SWAGGER` to `false` in the `api` section of your `app.ini`.
 For more information, refer to Gitea's [API docs](development/api-usage.md).
 
-You can see the latest API (for example) on https://try.gitea.io/api/swagger
+You can see the latest API (for example) on https://gitea.com/api/swagger
 
-You can also see an example of the `swagger.json` file at https://try.gitea.io/swagger.v1.json
+You can also see an example of the `swagger.json` file at https://gitea.com/swagger.v1.json
 
 ## Adjusting your server for public/private use
 
diff --git a/docs/content/help/faq.zh-cn.md b/docs/content/help/faq.zh-cn.md
index ef8a149ae2..d24dfe24a2 100644
--- a/docs/content/help/faq.zh-cn.md
+++ b/docs/content/help/faq.zh-cn.md
@@ -47,7 +47,7 @@ menu:
 
 为了迁移诸如问题、拉取请求等项目,您需要至少输入您的用户名。
 
-[Example (requires login)](https://try.gitea.io/repo/migrate)
+[Example (requires login)](https://demo.gitea.com/repo/migrate)
 
 要从GitLab迁移到Gitea,您可以使用这个非关联的工具:
 
@@ -141,9 +141,9 @@ Gitea不提供内置的Pages服务器。您需要一个专用的域名来提供
 但是,您可以在app.ini的api部分将ENABLE_SWAGGER设置为false,以禁用其文档显示。
 有关更多信息,请参阅Gitea的[API文档](development/api-usage.md)。
 
-您可以在上查看最新的API(例如)https://try.gitea.io/api/swagger
+您可以在上查看最新的API(例如)https://gitea.com/api/swagger
 
-您还可以在上查看`swagger.json`文件的示例 https://try.gitea.io/swagger.v1.json
+您还可以在上查看`swagger.json`文件的示例 https://gitea.com/swagger.v1.json
 
 ## 调整服务器用于公共/私有使用
 
diff --git a/docs/content/help/support.en-us.md b/docs/content/help/support.en-us.md
index db735b8124..bc8a8e3fd6 100644
--- a/docs/content/help/support.en-us.md
+++ b/docs/content/help/support.en-us.md
@@ -19,11 +19,11 @@ menu:
 
 - [Paid Commercial Support](https://about.gitea.com/)
 - [Discord](https://discord.gg/Gitea)
-- [Discourse Forum](https://discourse.gitea.io/)
+- [Forum](https://forum.gitea.com/)
 - [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
   - NOTE: Most of the Matrix channels are bridged with their counterpart in Discord and may experience some degree of flakiness with the bridge process.
 - Chinese Support
-  - [Discourse Chinese Category](https://discourse.gitea.io/c/5-category/5)
+  - [Discourse Chinese Category](https://forum.gitea.com/c/5-category/5)
   - QQ Group 328432459
 
 # Bug Report
@@ -39,7 +39,7 @@ If you found a bug, please [create an issue on GitHub](https://github.com/go-git
    - When using systemd, use `journalctl --lines 1000 --unit gitea` to collect logs.
    - When using docker, use `docker logs --tail 1000 <gitea-container>` to collect logs.
 4. Reproducible steps so that others could reproduce and understand the problem more quickly and easily.
-   - [try.gitea.io](https://try.gitea.io) could be used to reproduce the problem.
+   - [demo.gitea.com](https://demo.gitea.com) could be used to reproduce the problem.
 5. If you encounter slow/hanging/deadlock problems, please report the stacktrace when the problem occurs.
    Go to the "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report".
 
diff --git a/docs/content/help/support.zh-cn.md b/docs/content/help/support.zh-cn.md
index 91b37c586c..6c69584c67 100644
--- a/docs/content/help/support.zh-cn.md
+++ b/docs/content/help/support.zh-cn.md
@@ -19,11 +19,11 @@ menu:
 
 - [付费商业支持](https://about.gitea.com/)
 - [Discord](https://discord.gg/Gitea)
-- [Discourse 论坛](https://discourse.gitea.io/)
+- [论坛](https://forum.gitea.com/)
 - [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
   - 注意:大多数 Matrix 频道都与 Discord 中的对应频道桥接,可能在桥接过程中会出现一定程度的不稳定性。
 - 中文支持
-  - [Discourse 中文分类](https://discourse.gitea.io/c/5-category/5)
+  - [Discourse 中文分类](https://forum.gitea.com/c/5-category/5)
   - QQ 群 328432459
 
 # Bug 报告
@@ -39,7 +39,7 @@ menu:
    - 在使用 systemd 时,使用 `journalctl --lines 1000 --unit gitea` 收集日志。
    - 在使用 Docker 时,使用 `docker logs --tail 1000 <gitea-container>` 收集日志。
 4. 可重现的步骤,以便他人能够更快速、更容易地重现和理解问题。
-   - [try.gitea.io](https://try.gitea.io) 可用于重现问题。
+   - [demo.gitea.com](https://demo.gitea.com) 可用于重现问题。
 5. 如果遇到慢速/挂起/死锁等问题,请在出现问题时报告堆栈跟踪。
    转到 "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report"。
 
diff --git a/docs/content/index.en-us.md b/docs/content/index.en-us.md
index f9e6df8c1e..dc2eb1472a 100644
--- a/docs/content/index.en-us.md
+++ b/docs/content/index.en-us.md
@@ -21,7 +21,7 @@ up a self-hosted Git service.
 With Go, this can be done platform-independently across
 **all platforms** which Go supports, including Linux, macOS, and Windows,
 on x86, amd64, ARM and PowerPC architectures.
-You can try it out using [the online demo](https://try.gitea.io/).
+You can try it out using [the online demo](https://demo.gitea.com).
 
 ## Features
 
diff --git a/docs/content/usage/authentication.en-us.md b/docs/content/usage/authentication.en-us.md
index adc936dfbe..963f03a095 100644
--- a/docs/content/usage/authentication.en-us.md
+++ b/docs/content/usage/authentication.en-us.md
@@ -236,7 +236,7 @@ configure this, set the fields below:
 
   - Restrict what domains can log in if using a public SMTP host or SMTP host
     with multiple domains.
-  - Example: `gitea.io,mydomain.com,mydomain2.com`
+  - Example: `gitea.com,mydomain.com,mydomain2.com`
 
 - Force SMTPS
 
diff --git a/docs/content/usage/authentication.zh-cn.md b/docs/content/usage/authentication.zh-cn.md
index d1cfeeb800..00a24531d9 100644
--- a/docs/content/usage/authentication.zh-cn.md
+++ b/docs/content/usage/authentication.zh-cn.md
@@ -194,7 +194,7 @@ PAM提供了一种机制,通过对用户进行PAM认证来自动将其添加
 
   - 如果使用公共 SMTP 主机或有多个域的 SMTP 主机,限制哪些域可以登录
     限制哪些域可以登录。
-  - 示例: `gitea.io,mydomain.com,mydomain2.com`
+  - 示例: `gitea.com,mydomain.com,mydomain2.com`
 
 - 强制使用 SMTPS
   - 默认情况下将使用SMTPS连接到端口465.如果您希望将smtp用于其他端口,自行设置
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 e203c0d379..5220e0c7a0 100644
--- a/docs/content/usage/issue-pull-request-templates.en-us.md
+++ b/docs/content/usage/issue-pull-request-templates.en-us.md
@@ -308,7 +308,7 @@ This is a example for a issue config file
 blank_issues_enabled: true
 contact_links:
   - name: Gitea
-    url: https://gitea.io
+    url: https://gitea.com
     about: Visit the Gitea Website
 ```
 

From aa92b13164e84c26be91153b6022220ce0a27720 Mon Sep 17 00:00:00 2001
From: metiftikci <metiftikci@hotmail.com>
Date: Mon, 27 May 2024 18:34:18 +0300
Subject: [PATCH 052/131] Prevent simultaneous editing of comments and issues
 (#31053)

fixes #22907

Tested:
- [x] issue content edit
- [x] issue content change tasklist
- [x] pull request content edit
- [x] pull request change tasklist

![issue-content-edit](https://github.com/go-gitea/gitea/assets/29250154/a0828889-fb96-4bc4-8600-da92e3205812)
---
 models/issues/comment.go                      | 13 +++-
 models/issues/issue.go                        |  3 +
 models/issues/issue_update.go                 | 11 +++-
 models/migrations/migrations.go               |  4 ++
 models/migrations/v1_23/v299.go               | 18 +++++
 options/locale/locale_en-US.ini               |  4 ++
 routers/api/v1/repo/issue.go                  |  7 +-
 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/pull.go                   |  7 +-
 routers/web/repo/issue.go                     | 23 +++++--
 services/issue/comments.go                    |  4 +-
 services/issue/content.go                     |  4 +-
 templates/repo/diff/comments.tmpl             |  2 +-
 templates/repo/issue/view_content.tmpl        |  2 +-
 .../repo/issue/view_content/comments.tmpl     |  4 +-
 .../repo/issue/view_content/conversation.tmpl |  2 +-
 tests/integration/issue_test.go               | 66 +++++++++++++++++++
 web_src/js/features/repo-issue-edit.js        |  7 ++
 web_src/js/markup/tasklist.js                 | 12 +++-
 21 files changed, 172 insertions(+), 27 deletions(-)
 create mode 100644 models/migrations/v1_23/v299.go

diff --git a/models/issues/comment.go b/models/issues/comment.go
index 336bdde58e..c6c5dc2432 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -52,6 +52,8 @@ func (err ErrCommentNotExist) Unwrap() error {
 	return util.ErrNotExist
 }
 
+var ErrCommentAlreadyChanged = util.NewInvalidArgumentErrorf("the comment is already changed")
+
 // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
 type CommentType int
 
@@ -262,6 +264,7 @@ type Comment struct {
 	Line            int64 // - previous line / + proposed line
 	TreePath        string
 	Content         string        `xorm:"LONGTEXT"`
+	ContentVersion  int           `xorm:"NOT NULL DEFAULT 0"`
 	RenderedContent template.HTML `xorm:"-"`
 
 	// Path represents the 4 lines of code cemented by this comment
@@ -1111,7 +1114,7 @@ func UpdateCommentInvalidate(ctx context.Context, c *Comment) error {
 }
 
 // UpdateComment updates information of comment.
-func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error {
+func UpdateComment(ctx context.Context, c *Comment, contentVersion int, doer *user_model.User) error {
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
@@ -1119,9 +1122,15 @@ func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error
 	defer committer.Close()
 	sess := db.GetEngine(ctx)
 
-	if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
+	c.ContentVersion = contentVersion + 1
+
+	affected, err := sess.ID(c.ID).AllCols().Where("content_version = ?", contentVersion).Update(c)
+	if err != nil {
 		return err
 	}
+	if affected == 0 {
+		return ErrCommentAlreadyChanged
+	}
 	if err := c.LoadIssue(ctx); err != nil {
 		return err
 	}
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 87c1c86eb1..aad855522d 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -94,6 +94,8 @@ func (err ErrIssueWasClosed) Error() string {
 	return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
 }
 
+var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
+
 // Issue represents an issue or pull request of repository.
 type Issue struct {
 	ID               int64                  `xorm:"pk autoincr"`
@@ -107,6 +109,7 @@ type Issue struct {
 	Title            string                 `xorm:"name"`
 	Content          string                 `xorm:"LONGTEXT"`
 	RenderedContent  template.HTML          `xorm:"-"`
+	ContentVersion   int                    `xorm:"NOT NULL DEFAULT 0"`
 	Labels           []*Label               `xorm:"-"`
 	MilestoneID      int64                  `xorm:"INDEX"`
 	Milestone        *Milestone             `xorm:"-"`
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index 147b7eb3b9..31d76be5e0 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -235,7 +235,7 @@ func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string)
 }
 
 // ChangeIssueContent changes issue content, as the given user.
-func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string) (err error) {
+func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string, contentVersion int) (err error) {
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
@@ -254,9 +254,14 @@ func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User
 	}
 
 	issue.Content = content
+	issue.ContentVersion = contentVersion + 1
 
-	if err = UpdateIssueCols(ctx, issue, "content"); err != nil {
-		return fmt.Errorf("UpdateIssueCols: %w", err)
+	affected, err := db.GetEngine(ctx).ID(issue.ID).Cols("content", "content_version").Where("content_version = ?", contentVersion).Update(issue)
+	if err != nil {
+		return err
+	}
+	if affected == 0 {
+		return ErrIssueAlreadyChanged
 	}
 
 	if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 4501585250..08882fb119 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"
@@ -587,6 +588,9 @@ var migrations = []Migration{
 	NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
 
 	// Gitea 1.22.0-rc1 ends at 299
+
+	// v299 -> v300
+	NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_23/v299.go b/models/migrations/v1_23/v299.go
new file mode 100644
index 0000000000..f6db960c3b
--- /dev/null
+++ b/models/migrations/v1_23/v299.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 AddContentVersionToIssueAndComment(x *xorm.Engine) error {
+	type Issue struct {
+		ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
+	}
+
+	type Comment struct {
+		ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
+	}
+
+	return x.Sync(new(Comment), new(Issue))
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index fd47974fe9..772b11c2ba 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1443,6 +1443,7 @@ 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.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
 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
@@ -1758,6 +1759,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.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
 pulls.view = View Pull Request
 pulls.compare_changes = New Pull Request
 pulls.allow_edits_from_maintainers = Allow edits from maintainers
@@ -1903,6 +1905,8 @@ pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong>
 
 pull.deleted_branch = (deleted):%s
 
+comments.edit.already_changed = Unable to save changes to the comment. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
+
 milestones.new = New Milestone
 milestones.closed = Closed %s
 milestones.update_ago = Updated %s
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index b91fbc33bf..ddfc36f17d 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -810,8 +810,13 @@ func EditIssue(ctx *context.APIContext) {
 		}
 	}
 	if form.Body != nil {
-		err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
+		err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
 		if err != nil {
+			if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
+				ctx.Error(http.StatusBadRequest, "ChangeContent", err)
+				return
+			}
+
 			ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
 			return
 		}
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
index f5a28e6fa6..ef846a43a3 100644
--- a/routers/api/v1/repo/issue_attachment.go
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -198,7 +198,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
 
 	issue.Attachments = append(issue.Attachments, attachment)
 
-	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content); err != nil {
+	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content, issue.ContentVersion); err != nil {
 		ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
 		return
 	}
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 070571ba62..910cc1ce74 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -611,7 +611,7 @@ 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 {
+	if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
 		if errors.Is(err, user_model.ErrBlockedUser) {
 			ctx.Error(http.StatusForbidden, "UpdateComment", err)
 		} else {
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index 77aa7f0400..1ec758ec2c 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -210,7 +210,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
 		return
 	}
 
-	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
+	if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil {
 		if errors.Is(err, user_model.ErrBlockedUser) {
 			ctx.Error(http.StatusForbidden, "UpdateComment", err)
 		} else {
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 38a32a73c7..a9aa5c4d8e 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -610,8 +610,13 @@ func EditPullRequest(ctx *context.APIContext) {
 		}
 	}
 	if form.Body != nil {
-		err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
+		err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
 		if err != nil {
+			if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
+				ctx.Error(http.StatusBadRequest, "ChangeContent", err)
+				return
+			}
+
 			ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
 			return
 		}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 465dafefd3..ce459f23b9 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -2247,9 +2247,15 @@ func UpdateIssueContent(ctx *context.Context) {
 		return
 	}
 
-	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
+	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content"), ctx.FormInt("content_version")); err != nil {
 		if errors.Is(err, user_model.ErrBlockedUser) {
 			ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
+		} else if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
+			if issue.IsPull {
+				ctx.JSONError(ctx.Tr("repo.pulls.edit.already_changed"))
+			} else {
+				ctx.JSONError(ctx.Tr("repo.issues.edit.already_changed"))
+			}
 		} else {
 			ctx.ServerError("ChangeContent", err)
 		}
@@ -2278,8 +2284,9 @@ func UpdateIssueContent(ctx *context.Context) {
 	}
 
 	ctx.JSON(http.StatusOK, map[string]any{
-		"content":     content,
-		"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
+		"content":        content,
+		"contentVersion": issue.ContentVersion,
+		"attachments":    attachmentsHTML(ctx, issue.Attachments, issue.Content),
 	})
 }
 
@@ -3153,12 +3160,15 @@ func UpdateCommentContent(ctx *context.Context) {
 
 	oldContent := comment.Content
 	newContent := ctx.FormString("content")
+	contentVersion := ctx.FormInt("content_version")
 
 	// allow to save empty content
 	comment.Content = newContent
-	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
+	if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
 		if errors.Is(err, user_model.ErrBlockedUser) {
 			ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+		} else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
+			ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
 		} else {
 			ctx.ServerError("UpdateComment", err)
 		}
@@ -3198,8 +3208,9 @@ func UpdateCommentContent(ctx *context.Context) {
 	}
 
 	ctx.JSON(http.StatusOK, map[string]any{
-		"content":     renderedContent,
-		"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
+		"content":        renderedContent,
+		"contentVersion": comment.ContentVersion,
+		"attachments":    attachmentsHTML(ctx, comment.Attachments, comment.Content),
 	})
 }
 
diff --git a/services/issue/comments.go b/services/issue/comments.go
index d68623aff6..33b5702a00 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -82,7 +82,7 @@ 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 {
+func UpdateComment(ctx context.Context, c *issues_model.Comment, contentVersion int, doer *user_model.User, oldContent string) error {
 	if err := c.LoadIssue(ctx); err != nil {
 		return err
 	}
@@ -110,7 +110,7 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_mode
 		}
 	}
 
-	if err := issues_model.UpdateComment(ctx, c, doer); err != nil {
+	if err := issues_model.UpdateComment(ctx, c, contentVersion, doer); err != nil {
 		return err
 	}
 
diff --git a/services/issue/content.go b/services/issue/content.go
index 2f9bee806a..6894182909 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -13,7 +13,7 @@ import (
 )
 
 // ChangeContent changes issue content, as the given user.
-func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
+func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string, contentVersion int) error {
 	if err := issue.LoadRepo(ctx); err != nil {
 		return err
 	}
@@ -26,7 +26,7 @@ func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_mo
 
 	oldContent := issue.Content
 
-	if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
+	if err := issues_model.ChangeIssueContent(ctx, issue, doer, content, contentVersion); err != nil {
 		return err
 	}
 
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index c7f4337182..90d6a511bf 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -61,7 +61,7 @@
 			{{end}}
 			</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>
+			<div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" 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/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index d40134ed08..3088c60510 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -60,7 +60,7 @@
 							{{end}}
 						</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>
+						<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-content-version="{{.Issue.ContentVersion}}" 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}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index acc04e4c61..3da2f3815e 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -67,7 +67,7 @@
 							{{end}}
 						</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>
+						<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" 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}}
@@ -441,7 +441,7 @@
 								{{end}}
 							</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>
+							<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" 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 ac32a2db5d..43ec9d75c4 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -96,7 +96,7 @@
 								{{end}}
 								</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>
+								<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" 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/tests/integration/issue_test.go b/tests/integration/issue_test.go
index d74516d110..308b82d4b9 100644
--- a/tests/integration/issue_test.go
+++ b/tests/integration/issue_test.go
@@ -191,6 +191,34 @@ func TestNewIssue(t *testing.T) {
 	testNewIssue(t, session, "user2", "repo1", "Title", "Description")
 }
 
+func TestEditIssue(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	session := loginUser(t, "user2")
+	issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
+
+	req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
+		"_csrf":   GetCSRF(t, session, issueURL),
+		"content": "modified content",
+		"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
+	})
+	session.MakeRequest(t, req, http.StatusOK)
+
+	req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
+		"_csrf":   GetCSRF(t, session, issueURL),
+		"content": "modified content",
+		"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
+	})
+	session.MakeRequest(t, req, http.StatusBadRequest)
+
+	req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
+		"_csrf":           GetCSRF(t, session, issueURL),
+		"content":         "modified content",
+		"content_version": "1",
+		"context":         fmt.Sprintf("/%s/%s", "user2", "repo1"),
+	})
+	session.MakeRequest(t, req, http.StatusOK)
+}
+
 func TestIssueCommentClose(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user2")
@@ -257,6 +285,44 @@ func TestIssueCommentUpdate(t *testing.T) {
 	assert.Equal(t, modifiedContent, comment.Content)
 }
 
+func TestIssueCommentUpdateSimultaneously(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	session := loginUser(t, "user2")
+	issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
+	comment1 := "Test comment 1"
+	commentID := testIssueAddComment(t, session, issueURL, comment1, "")
+
+	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
+	assert.Equal(t, comment1, comment.Content)
+
+	modifiedContent := comment.Content + "MODIFIED"
+
+	req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
+		"_csrf":   GetCSRF(t, session, issueURL),
+		"content": modifiedContent,
+	})
+	session.MakeRequest(t, req, http.StatusOK)
+
+	modifiedContent = comment.Content + "2"
+
+	req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
+		"_csrf":   GetCSRF(t, session, issueURL),
+		"content": modifiedContent,
+	})
+	session.MakeRequest(t, req, http.StatusBadRequest)
+
+	req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
+		"_csrf":           GetCSRF(t, session, issueURL),
+		"content":         modifiedContent,
+		"content_version": "1",
+	})
+	session.MakeRequest(t, req, http.StatusOK)
+
+	comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
+	assert.Equal(t, modifiedContent, comment.Content)
+	assert.Equal(t, 2, comment.ContentVersion)
+}
+
 func TestIssueReaction(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user2")
diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js
index abf2d31221..9a8d737e01 100644
--- a/web_src/js/features/repo-issue-edit.js
+++ b/web_src/js/features/repo-issue-edit.js
@@ -3,6 +3,7 @@ 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 {showErrorToast} from '../modules/toast.js';
 import {hideElem, showElem} from '../utils/dom.js';
 import {attachRefIssueContextPopup} from './contextpopup.js';
 import {initCommentContent, initMarkupContent} from '../markup/content.js';
@@ -124,11 +125,17 @@ async function onEditContent(event) {
       const params = new URLSearchParams({
         content: comboMarkdownEditor.value(),
         context: editContentZone.getAttribute('data-context'),
+        content_version: editContentZone.getAttribute('data-content-version'),
       });
       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 (response.status === 400) {
+        showErrorToast(data.errorMessage);
+        return;
+      }
+      editContentZone.setAttribute('data-content-version', data.contentVersion);
       if (!data.content) {
         renderContent.innerHTML = document.getElementById('no-content').innerHTML;
         rawContent.textContent = '';
diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js
index 00076bce58..a40b5e4abd 100644
--- a/web_src/js/markup/tasklist.js
+++ b/web_src/js/markup/tasklist.js
@@ -1,4 +1,5 @@
 import {POST} from '../modules/fetch.js';
+import {showErrorToast} from '../modules/toast.js';
 
 const preventListener = (e) => e.preventDefault();
 
@@ -54,13 +55,20 @@ export function initMarkupTasklist() {
           const editContentZone = container.querySelector('.edit-content-zone');
           const updateUrl = editContentZone.getAttribute('data-update-url');
           const context = editContentZone.getAttribute('data-context');
+          const contentVersion = editContentZone.getAttribute('data-content-version');
 
           const requestBody = new FormData();
           requestBody.append('ignore_attachments', 'true');
           requestBody.append('content', newContent);
           requestBody.append('context', context);
-          await POST(updateUrl, {data: requestBody});
-
+          requestBody.append('content_version', contentVersion);
+          const response = await POST(updateUrl, {data: requestBody});
+          const data = await response.json();
+          if (response.status === 400) {
+            showErrorToast(data.errorMessage);
+            return;
+          }
+          editContentZone.setAttribute('data-content-version', data.contentVersion);
           rawContent.textContent = newContent;
         } catch (err) {
           checkbox.checked = !checkbox.checked;

From 0222f19f19675afcc0e38237618a712908e3852c Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 28 May 2024 00:26:53 +0000
Subject: [PATCH 053/131] [skip ci] Updated translations via Crowdin

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

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 6314b62f66..0c61e5d042 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -1205,7 +1205,7 @@ branches=Větve
 tags=Značky
 issues=Úkoly
 pulls=Pull requesty
-project_board=Projekty
+projects=Projekty
 packages=Balíčky
 actions=Akce
 labels=Štítky
@@ -1364,8 +1364,6 @@ commitstatus.success=Úspěch
 ext_issues=Přístup k externím úkolům
 ext_issues.desc=Odkaz na externí systém úkolů.
 
-projects=Projekty
-projects.desc=Spravovat úkoly a požadavky na natažení na projektových nástěnkách.
 projects.description=Popis (volitelné)
 projects.description_placeholder=Popis
 projects.create=Vytvořit projekt
@@ -1887,6 +1885,7 @@ pulls.recently_pushed_new_branches=Nahráli jste větev <strong>%[1]s</strong> %
 
 pull.deleted_branch=(odstraněno):%s
 
+
 milestones.new=Nový milník
 milestones.closed=Zavřen dne %s
 milestones.update_ago=Aktualizováno %s
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 5bca84ca08..8e1194cdd1 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -1206,7 +1206,7 @@ branches=Branches
 tags=Tags
 issues=Issues
 pulls=Pull-Requests
-project_board=Projekte
+projects=Projekte
 packages=Pakete
 actions=Actions
 labels=Label
@@ -1366,8 +1366,6 @@ commitstatus.success=Erfolg
 ext_issues=Zugriff auf Externe Issues
 ext_issues.desc=Link zu externem Issuetracker.
 
-projects=Projekte
-projects.desc=Verwalte Issues und Pull-Requests in Projektboards.
 projects.description=Beschreibung (optional)
 projects.description_placeholder=Beschreibung
 projects.create=Projekt erstellen
@@ -1891,6 +1889,7 @@ pulls.recently_pushed_new_branches=Du hast auf den Branch <strong>%[1]s</strong>
 
 pull.deleted_branch=(gelöscht):%s
 
+
 milestones.new=Neuer Meilenstein
 milestones.closed=Geschlossen %s
 milestones.update_ago=%s aktualisiert
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 834d1d7d70..74262ff38d 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -1137,7 +1137,7 @@ branches=Κλάδοι
 tags=Ετικέτες
 issues=Ζητήματα
 pulls=Pull Requests
-project_board=Έργα
+projects=Έργα
 packages=Πακέτα
 actions=Δράσεις
 labels=Σήματα
@@ -1292,8 +1292,6 @@ commitstatus.success=Επιτυχές
 ext_issues=Πρόσβαση στα Εξωτερικά Ζητήματα
 ext_issues.desc=Σύνδεση σε εξωτερικό εφαρμογή ζητημάτων.
 
-projects=Έργα
-projects.desc=Διαχείριση ζητημάτων και pulls στους πίνακες των έργων.
 projects.description=Περιγραφή (προαιρετικό)
 projects.description_placeholder=Περιγραφή
 projects.create=Δημιουργία Έργου
@@ -1810,6 +1808,7 @@ pulls.recently_pushed_new_branches=Ωθήσατε στο κλάδο <strong>%[1]
 
 pull.deleted_branch=(διαγράφηκε):%s
 
+
 milestones.new=Νέο Ορόσημο
 milestones.closed=Έκλεισε %s
 milestones.update_ago=Ενημερώθηκε %s
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 3894e0e85b..66273eb79a 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -1130,7 +1130,7 @@ branches=Ramas
 tags=Etiquetas
 issues=Incidencias
 pulls=Pull Requests
-project_board=Proyectos
+projects=Proyectos
 packages=Paquetes
 actions=Acciones
 labels=Etiquetas
@@ -1285,8 +1285,6 @@ commitstatus.success=Éxito
 ext_issues=Acceso a incidencias externas
 ext_issues.desc=Enlace a un gestor de incidencias externo.
 
-projects=Proyectos
-projects.desc=Gestionar problemas y pulls en los tablones del proyecto.
 projects.description=Descripción (opcional)
 projects.description_placeholder=Descripción
 projects.create=Crear Proyecto
@@ -1796,6 +1794,7 @@ pulls.recently_pushed_new_branches=Has realizado push en la rama <strong>%[1]s</
 
 pull.deleted_branch=(eliminado):%s
 
+
 milestones.new=Nuevo hito
 milestones.closed=Cerrada %s
 milestones.update_ago=Actualizado %s
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index d720ecf2f8..94e572f9b4 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -891,7 +891,7 @@ branches=شاخه‎ها
 tags=برچسب‎ها
 issues=مسائل
 pulls=تقاضاهای واکشی
-project_board=پروژه‌ها
+projects=پروژه‌ها
 labels=برچسب‌ها
 org_labels_desc=برچسب های سطح سازمان که می توانند برای <strong>تمامی مخازن</strong> ذیل این سازمان استفاده شوند
 org_labels_desc_manage=مدیریت
@@ -986,8 +986,6 @@ commitstatus.pending=در انتظار
 
 ext_issues.desc=پیوند به ردیاب خارجی برای موضوع.
 
-projects=پروژه‌ها
-projects.desc=مدیریت مشکلات و درخواست‌های درج در بورد پروژه.
 projects.description=توضیحات (دلخواه)
 projects.description_placeholder=توضیحات
 projects.create=ایجاد پروژه جدید
@@ -1377,6 +1375,7 @@ pulls.reopened_at=`این درخواست pull را بازگشایی کرد <a id
 
 
 
+
 milestones.new=نقطه عطف جدید
 milestones.closed=%s بسته شد
 milestones.no_due_date=بدون موعد مقرر
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index f29ad8c6cd..d854e74e61 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -730,7 +730,7 @@ branches=Branchit
 tags=Tagit
 issues=Ongelmat
 pulls=Pull-pyynnöt
-project_board=Projektit
+projects=Projektit
 packages=Paketit
 labels=Tunnisteet
 
@@ -792,7 +792,6 @@ commitstatus.error=Virhe
 commitstatus.pending=Odottaa
 
 
-projects=Projektit
 projects.description_placeholder=Kuvaus
 projects.create=Luo projekti
 projects.title=Otsikko
@@ -1002,6 +1001,7 @@ pulls.can_auto_merge_desc=Tämä pull-pyyntö voidaan yhdistää automaattisesti
 
 
 
+
 milestones.new=Uusi merkkipaalu
 milestones.closed=Suljettu %s
 milestones.no_due_date=Ei määräpäivää
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 556fab28e8..0def8f81d1 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1149,7 +1149,7 @@ branches=Branches
 tags=Étiquettes
 issues=Tickets
 pulls=Demandes d'ajout
-project_board=Projets
+projects=Projets
 packages=Paquets
 actions=Actions
 labels=Labels
@@ -1306,8 +1306,6 @@ commitstatus.success=Succès
 ext_issues=Accès aux tickets externes
 ext_issues.desc=Lien vers un gestionnaire de tickets externe.
 
-projects=Projets
-projects.desc=Gérer les tickets et les demandes d’ajouts dans les tableaux de projet.
 projects.description=Description (facultative)
 projects.description_placeholder=Description
 projects.create=Créer un projet
@@ -1826,6 +1824,7 @@ pulls.recently_pushed_new_branches=Vous avez soumis sur la branche <strong>%[1]s
 
 pull.deleted_branch=(supprimé) : %s
 
+
 milestones.new=Nouveau jalon
 milestones.closed=%s fermé
 milestones.update_ago=Actualisé %s
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 4e46227fea..06eb31f308 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -670,7 +670,7 @@ branches=Ágak
 tags=Címkék
 issues=Hibajegyek
 pulls=Egyesítési kérések
-project_board=Projektek
+projects=Projektek
 labels=Címkék
 org_labels_desc_manage=kezelés
 
@@ -736,7 +736,6 @@ commitstatus.pending=Függőben
 
 ext_issues.desc=Külső hibakövető csatlakoztatás.
 
-projects=Projektek
 projects.description_placeholder=Leírás
 projects.title=Cím
 projects.new=Új projekt
@@ -949,6 +948,7 @@ pulls.status_checks_success=Minden ellenőrzés sikeres volt
 
 
 
+
 milestones.new=Új mérföldkő
 milestones.closed=Lezárva: %s
 milestones.no_due_date=Nincs határidő
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index fe3a6d0b08..a6bac362ab 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -763,6 +763,7 @@ pulls.can_auto_merge_desc=Permintaan tarik ini dapat digabung secara otomatis.
 
 
 
+
 milestones.new=Milestone Baru
 milestones.closed=Tertutup %s
 milestones.no_due_date=Tidak ada jatuh tempo
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index f2fcfb7eda..f6becbf1c0 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -660,7 +660,7 @@ branches=Greinar
 tags=Merki
 issues=Vandamál
 pulls=Sameiningarbeiðnir
-project_board=Verkefni
+projects=Verkefni
 packages=Pakkar
 labels=Skýringar
 
@@ -714,7 +714,6 @@ commitstatus.error=Villa
 commitstatus.pending=Í bið
 
 
-projects=Verkefni
 projects.description=Lýsing (valfrjálst)
 projects.description_placeholder=Lýsing
 projects.create=Stofna Verkefni
@@ -912,6 +911,7 @@ pulls.status_checks_details=Nánar
 
 
 
+
 milestones.new=Nýtt tímamót
 milestones.closed=Lokaði %s
 milestones.no_due_date=Enginn eindagi
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index 0cecc0b7f3..a32ae01868 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -954,7 +954,7 @@ branches=Rami (Branch)
 tags=Tag
 issues=Problemi
 pulls=Pull Requests
-project_board=Progetti
+projects=Progetti
 packages=Pacchetti
 labels=Etichette
 org_labels_desc=Etichette a livello di organizzazione che possono essere utilizzate con <strong>tutti i repository</strong> sotto questa organizzazione
@@ -1072,8 +1072,6 @@ commitstatus.pending=In sospeso
 ext_issues=Accesso ai Problemi Esterni
 ext_issues.desc=Collegamento al puntatore di una issue esterna.
 
-projects=Progetti
-projects.desc=Gestisci problemi e pull nelle schede di progetto.
 projects.description=Descrizione (opzionale)
 projects.description_placeholder=Descrizione
 projects.create=Crea un progetto
@@ -1500,6 +1498,7 @@ pulls.delete.text=Vuoi davvero eliminare questo problema? (Questo rimuoverà per
 
 
 
+
 milestones.new=Nuova Milestone
 milestones.closed=Chiuso %s
 milestones.no_due_date=Nessuna data di scadenza
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 66dedcbb51..07c1cbfe7e 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -1215,7 +1215,7 @@ branches=ブランチ
 tags=タグ
 issues=イシュー
 pulls=プルリクエスト
-project_board=プロジェクト
+projects=プロジェクト
 packages=パッケージ
 actions=Actions
 labels=ラベル
@@ -1378,8 +1378,6 @@ commitstatus.success=成功
 ext_issues=外部イシューへのアクセス
 ext_issues.desc=外部のイシュートラッカーへのリンク。
 
-projects=プロジェクト
-projects.desc=プロジェクトボードでイシューとプルを管理します。
 projects.description=説明 (オプション)
 projects.description_placeholder=説明
 projects.create=プロジェクトを作成
@@ -1903,6 +1901,7 @@ pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1
 
 pull.deleted_branch=(削除済み):%s
 
+
 milestones.new=新しいマイルストーン
 milestones.closed=%s にクローズ
 milestones.update_ago=%sに更新
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index cf3188e9c0..054632e819 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -862,6 +862,7 @@ pulls.invalid_merge_option=이 풀 리퀘스트에서 설정한 머지 옵션을
 
 
 
+
 milestones.new=새로운 마일스톤
 milestones.closed=닫힘 %s
 milestones.no_due_date=기한 없음
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index bdfe3f8c9f..8f9766b082 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -1139,7 +1139,7 @@ branches=Atzari
 tags=Tagi
 issues=Problēmas
 pulls=Izmaiņu pieprasījumi
-project_board=Projekti
+projects=Projekti
 packages=Pakotnes
 actions=Darbības
 labels=Iezīmes
@@ -1294,8 +1294,6 @@ commitstatus.success=Pabeigts
 ext_issues=Piekļuve ārējām problēmām
 ext_issues.desc=Saite uz ārējo problēmu sekotāju.
 
-projects=Projekti
-projects.desc=Pārvaldīt problēmu un izmaiņu pieprasījumu projektu dēļus.
 projects.description=Apraksts (neobligāts)
 projects.description_placeholder=Apraksts
 projects.create=Izveidot projektu
@@ -1812,6 +1810,7 @@ pulls.recently_pushed_new_branches=Tu iesūtīji izmaiņas atzarā <strong>%[1]s
 
 pull.deleted_branch=(izdzēsts):%s
 
+
 milestones.new=Jauns atskaites punkts
 milestones.closed=Aizvērts %s
 milestones.update_ago=Atjaunots %s
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index f511bc5d23..adcbc6b66d 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -952,7 +952,7 @@ branches=Branches
 tags=Labels
 issues=Kwesties
 pulls=Pull-aanvragen
-project_board=Projecten
+projects=Projecten
 packages=Paketten
 labels=Labels
 org_labels_desc=Organisatielabel dat gebruikt kan worden met <strong>alle repositories</strong> onder deze organisatie
@@ -1070,8 +1070,6 @@ commitstatus.pending=In behandeling
 ext_issues=Toegang tot Externe Issues
 ext_issues.desc=Koppelen aan een externe kwestie-tracker.
 
-projects=Projecten
-projects.desc=Beheer issues en pulls in projectborden.
 projects.description=Omschrijving (optioneel)
 projects.description_placeholder=Omschrijving
 projects.create=Project aanmaken
@@ -1495,6 +1493,7 @@ pulls.delete.text=Weet je zeker dat je deze pull-verzoek wilt verwijderen? (Dit
 
 
 
+
 milestones.new=Nieuwe mijlpaal
 milestones.closed=%s werd gesloten
 milestones.no_due_date=Geen vervaldatum
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index b5a758514e..6fdec5183e 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -894,7 +894,7 @@ branches=Gałęzie
 tags=Tagi
 issues=Zgłoszenia
 pulls=Oczekujące zmiany
-project_board=Projekty
+projects=Projekty
 labels=Etykiety
 org_labels_desc=Etykiety organizacji, które mogą być używane z <strong>wszystkimi repozytoriami</strong> w tej organizacji
 org_labels_desc_manage=zarządzaj
@@ -987,7 +987,6 @@ commitstatus.pending=Oczekująca
 
 ext_issues.desc=Link do zewnętrznego systemu śledzenia zgłoszeń.
 
-projects=Projekty
 projects.description=Opis (opcjonalnie)
 projects.description_placeholder=Opis
 projects.create=Utwórz projekt
@@ -1347,6 +1346,7 @@ pulls.reopened_at=`otworzył(-a) ponownie ten Pull Request <a id="%[1]s" href="#
 
 
 
+
 milestones.new=Nowy kamień milowy
 milestones.closed=Zamknięto %s
 milestones.no_due_date=Nie ustalono terminu
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 4799727d98..222abc1681 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -1134,7 +1134,7 @@ branches=Branches
 tags=Tags
 issues=Issues
 pulls=Pull requests
-project_board=Projetos
+projects=Projetos
 packages=Pacotes
 actions=Ações
 labels=Etiquetas
@@ -1290,8 +1290,6 @@ commitstatus.success=Sucesso
 ext_issues=Acesso a Issues Externos
 ext_issues.desc=Link para o issue tracker externo.
 
-projects=Projetos
-projects.desc=Gerencie issues e PRs nos quadros do projeto.
 projects.description=Descrição (opcional)
 projects.description_placeholder=Descrição
 projects.create=Criar Projeto
@@ -1804,6 +1802,7 @@ pulls.recently_pushed_new_branches=Você fez push no branch <strong>%[1]s</stron
 
 pull.deleted_branch=(excluído):%s
 
+
 milestones.new=Novo marco
 milestones.closed=Fechado %s
 milestones.update_ago=Atualizado há %s
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 15635b4beb..28f040e7cf 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1215,7 +1215,7 @@ branches=Ramos
 tags=Etiquetas
 issues=Questões
 pulls=Pedidos de integração
-project_board=Planeamentos
+projects=Planeamentos
 packages=Pacotes
 actions=Operações
 labels=Rótulos
@@ -1378,8 +1378,6 @@ commitstatus.success=Sucesso
 ext_issues=Acesso a questões externas
 ext_issues.desc=Ligação para um rastreador de questões externo.
 
-projects=Planeamentos
-projects.desc=Gerir questões e integrações nos quadros do planeamento.
 projects.description=Descrição (opcional)
 projects.description_placeholder=Descrição
 projects.create=Criar planeamento
@@ -1903,6 +1901,7 @@ pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2
 
 pull.deleted_branch=(eliminado):%s
 
+
 milestones.new=Nova etapa
 milestones.closed=Encerrada %s
 milestones.update_ago=Modificou %s
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 81b88dbd45..33634105ff 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -1117,7 +1117,7 @@ branches=Ветки
 tags=Теги
 issues=Задачи
 pulls=Запросы на слияние
-project_board=Проекты
+projects=Проекты
 packages=Пакеты
 actions=Действия
 labels=Метки
@@ -1269,8 +1269,6 @@ commitstatus.success=Успешно
 ext_issues=Доступ к внешним задачам
 ext_issues.desc=Ссылка на внешнюю систему отслеживания ошибок.
 
-projects=Проекты
-projects.desc=Управление задачами и pull'ами в досках проекта.
 projects.description=Описание (необязательно)
 projects.description_placeholder=Описание
 projects.create=Создать проект
@@ -1774,6 +1772,7 @@ pulls.delete.text=Вы действительно хотите удалить э
 
 pull.deleted_branch=(удалена):%s
 
+
 milestones.new=Новый этап
 milestones.closed=Закрыт %s
 milestones.update_ago=Обновлено %s
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index cb437e5530..16c11ef713 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -863,7 +863,7 @@ branches=ශාඛා
 tags=ටැග්
 issues=ගැටළු
 pulls=ඉල්ලීම් අදින්න
-project_board=ව්‍යාපෘති
+projects=ව්‍යාපෘති
 labels=ලේබල
 org_labels_desc=මෙම සංවිධානය යටතේ <strong>සියලුම ගබඩාවලදී</strong> සමඟ භාවිතා කළ හැකි සංවිධාන මට්ටමේ ලේබල්
 org_labels_desc_manage=කළමනාකරණය
@@ -958,8 +958,6 @@ commitstatus.pending=වංගු
 
 ext_issues.desc=බාහිර නිකුතුවකට සම්බන්ධ වන්න ට්රැකර්.
 
-projects=ව්‍යාපෘති
-projects.desc=ව්යාපෘති මණ්ඩලවල ගැටළු සහ අදින කළමනාකරණය කිරීම.
 projects.description=විස්තරය (විකල්ප)
 projects.description_placeholder=සවිස්තරය
 projects.create=ව්‍යාපෘතිය සාදන්න
@@ -1340,6 +1338,7 @@ pulls.reopened_at=`මෙම අදින්න ඉල්ලීම නැවත
 
 
 
+
 milestones.new=නව සන්ධිස්ථානයක්
 milestones.closed=%s වසා ඇත
 milestones.no_due_date=නියමිත දිනයක් නැත
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index be1efa22bc..079523e38c 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -981,7 +981,7 @@ find_tag=Hľadať tag
 branches=Vetvy
 tags=Tagy
 pulls=Pull requesty
-project_board=Projekty
+projects=Projekty
 packages=Balíčky
 actions=Akcie
 labels=Štítky
@@ -1050,7 +1050,6 @@ commit.cherry-pick-content=Vyberte vetvu pre cherry-pick na:
 commitstatus.error=Chyba
 
 
-projects=Projekty
 projects.title=Názov
 projects.new=Nový projekt
 projects.deletion=Vymazať projekt
@@ -1121,6 +1120,7 @@ pulls.merge_commit_id=ID zlučovacieho commitu
 
 
 
+
 milestones.open=Otvoriť
 milestones.close=Zavrieť
 milestones.cancel=Zrušiť
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index b975636cb8..ee729911c3 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -734,7 +734,7 @@ branches=Grenar
 tags=Taggar
 issues=Ärenden
 pulls=Pull-förfrågningar
-project_board=Projekt
+projects=Projekt
 labels=Etiketter
 org_labels_desc=Etiketter på organisationsnivå som kan användas i <strong>alla utvecklingskataloger</strong> tillhörande denna organisation
 org_labels_desc_manage=hantera
@@ -814,7 +814,6 @@ commitstatus.pending=Väntande
 
 ext_issues.desc=Länk till externt ärendehanteringssystem.
 
-projects=Projekt
 projects.description_placeholder=Beskrivning
 projects.create=Skapa projekt
 projects.title=Titel
@@ -1119,6 +1118,7 @@ pulls.outdated_with_base_branch=Denna branch är föråldrad gentemot bas-branch
 
 
 
+
 milestones.new=Ny milstolpe
 milestones.closed=Stängt %s
 milestones.no_due_date=Inget förfallodatum
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 7b57e416f7..1cb056f578 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -763,6 +763,7 @@ manage_themes=Varsayılan temayı seç
 manage_openid=OpenID Adreslerini Yönet
 email_desc=Ana e-posta adresiniz bildirimler, parola kurtarma ve gizlenmemişse eğer web tabanlı Git işlemleri için kullanılacaktır.
 theme_desc=Bu, sitedeki varsayılan temanız olacak.
+theme_colorblindness_help=Renk Körlüğü için Tema Desteği
 primary=Birincil
 activated=Aktifleştirildi
 requires_activation=Etkinleştirme gerekiyor
@@ -1212,7 +1213,7 @@ branches=Dal
 tags=Etiket
 issues=Konular
 pulls=Değişiklik İstekleri
-project_board=Projeler
+projects=Projeler
 packages=Paketler
 actions=İşlemler
 labels=Etiketler
@@ -1375,8 +1376,6 @@ commitstatus.success=Başarılı
 ext_issues=Harici Konulara Erişim
 ext_issues.desc=Dışsal konu takip sistemine bağla.
 
-projects=Projeler
-projects.desc=Proje panolarındaki konuları ve değişiklikleri yönetin.
 projects.description=Açıklama (isteğe bağlı)
 projects.description_placeholder=Açıklama
 projects.create=Proje Oluştur
@@ -1900,6 +1899,7 @@ pulls.recently_pushed_new_branches=<strong>%[1]s</strong> dalına ittiniz %[2]s
 
 pull.deleted_branch=(silindi): %s
 
+
 milestones.new=Yeni Kilometre Taşı
 milestones.closed=Kapalı %s
 milestones.update_ago=%s tarihinde güncellendi
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index ddd884e113..cc06c87d32 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -899,7 +899,7 @@ branches=Гілки
 tags=Теги
 issues=Задачі
 pulls=Запити на злиття
-project_board=Проєкти
+projects=Проєкти
 labels=Мітки
 org_labels_desc=Мітки рівня організації можуть використовуватися <strong>в усіх репозиторіях</strong> цієї організації
 org_labels_desc_manage=керувати
@@ -995,8 +995,6 @@ commitstatus.pending=Очікування
 ext_issues=Доступ до зовнішніх задач
 ext_issues.desc=Посилання на зовнішню систему відстеження задач.
 
-projects=Проєкти
-projects.desc=Керуйте задачами та запитами злиття на дошках проєкту.
 projects.description=Опис (необов'язково)
 projects.description_placeholder=Опис
 projects.create=Створити проєкт
@@ -1387,6 +1385,7 @@ pulls.reopened_at=`повторно відкрив цей запит на зли
 
 
 
+
 milestones.new=Новий етап
 milestones.closed=Закрито %s
 milestones.no_due_date=Немає дати завершення
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 75facb4dcb..2d191521d6 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1215,7 +1215,7 @@ branches=分支列表
 tags=标签列表
 issues=工单
 pulls=合并请求
-project_board=项目
+projects=项目
 packages=软件包
 actions=Actions
 labels=标签
@@ -1378,8 +1378,6 @@ commitstatus.success=成功
 ext_issues=访问外部工单
 ext_issues.desc=链接到外部工单跟踪系统。
 
-projects=项目
-projects.desc=在项目看板中管理工单和合并请求。
 projects.description=描述(可选)
 projects.description_placeholder=描述
 projects.create=创建项目
@@ -1903,6 +1901,7 @@ pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]
 
 pull.deleted_branch=(已删除): %s
 
+
 milestones.new=新的里程碑
 milestones.closed=于 %s关闭
 milestones.update_ago=已更新 %s
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index fb16b82fc5..a6ae0ffe8e 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -500,6 +500,7 @@ pulls.can_auto_merge_desc=這個拉請求可以自動合併。
 
 
 
+
 milestones.new=新的里程碑
 milestones.closed=於 %s關閉
 milestones.no_due_date=暫無截止日期
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 50c0276567..c3590b6acc 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -1035,7 +1035,7 @@ branches=分支
 tags=標籤
 issues=問題
 pulls=合併請求
-project_board=專案
+projects=專案
 packages=套件
 actions=Actions
 labels=標籤
@@ -1176,8 +1176,6 @@ commitstatus.success=成功
 ext_issues=存取外部問題
 ext_issues.desc=連結到外部問題追蹤器。
 
-projects=專案
-projects.desc=在專案看板中管理問題與合併請求。
 projects.description=描述 (選用)
 projects.description_placeholder=描述
 projects.create=建立專案
@@ -1641,6 +1639,7 @@ pulls.delete.text=您真的要刪除此合併請求嗎?(這將會永久移除
 
 
 
+
 milestones.new=新增里程碑
 milestones.closed=於 %s關閉
 milestones.update_ago=已更新 %s

From b6b32a55295b121c44b81223a2d1ab331c210e81 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 28 May 2024 03:50:28 +0200
Subject: [PATCH 054/131] Update JS dependencies (#31120)

- Add `eslint-plugin-no-use-extend-native` to exclude list because it
requires flat config
- Exclude `@github/text-expander-element` because new version has broken
positioning
- Tested mermaid, monaco, swagger, chartjs
---
 package-lock.json | 632 +++++++++++++++++++++++-----------------------
 package.json      |  26 +-
 updates.config.js |   1 +
 3 files changed, 331 insertions(+), 328 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f535c318fa..90cedd63d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,11 +18,11 @@
         "add-asset-webpack-plugin": "3.0.0",
         "ansi_up": "6.0.2",
         "asciinema-player": "3.7.1",
-        "chart.js": "4.4.2",
+        "chart.js": "4.4.3",
         "chartjs-adapter-dayjs-4": "1.0.4",
         "chartjs-plugin-zoom": "2.0.1",
         "clippie": "4.1.1",
-        "css-loader": "7.1.1",
+        "css-loader": "7.1.2",
         "dayjs": "1.11.11",
         "dropzone": "6.0.0-beta.2",
         "easymde": "2.18.0",
@@ -34,17 +34,17 @@
         "jquery": "3.7.1",
         "katex": "0.16.10",
         "license-checker-webpack-plugin": "0.2.1",
-        "mermaid": "10.9.0",
+        "mermaid": "10.9.1",
         "mini-css-extract-plugin": "2.9.0",
         "minimatch": "9.0.4",
-        "monaco-editor": "0.48.0",
+        "monaco-editor": "0.49.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.2",
+        "postcss-nesting": "12.1.5",
         "sortablejs": "1.15.2",
-        "swagger-ui-dist": "5.17.7",
+        "swagger-ui-dist": "5.17.13",
         "tailwindcss": "3.4.3",
         "temporal-polyfill": "0.2.4",
         "throttle-debounce": "5.0.0",
@@ -64,19 +64,19 @@
       },
       "devDependencies": {
         "@eslint-community/eslint-plugin-eslint-comments": "4.3.0",
-        "@playwright/test": "1.44.0",
+        "@playwright/test": "1.44.1",
         "@stoplight/spectral-cli": "6.11.1",
         "@stylistic/eslint-plugin-js": "2.1.0",
         "@stylistic/stylelint-plugin": "2.1.2",
         "@vitejs/plugin-vue": "5.0.4",
         "eslint": "8.57.0",
         "eslint-plugin-array-func": "4.0.0",
-        "eslint-plugin-github": "4.10.2",
+        "eslint-plugin-github": "5.0.0-2",
         "eslint-plugin-i": "2.29.1",
         "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.5.0",
+        "eslint-plugin-regexp": "2.6.0",
         "eslint-plugin-sonarjs": "1.0.3",
         "eslint-plugin-unicorn": "53.0.0",
         "eslint-plugin-vitest": "0.4.1",
@@ -84,15 +84,15 @@
         "eslint-plugin-vue": "9.26.0",
         "eslint-plugin-vue-scoped-css": "2.8.0",
         "eslint-plugin-wc": "2.1.0",
-        "happy-dom": "14.10.1",
-        "markdownlint-cli": "0.40.0",
+        "happy-dom": "14.11.1",
+        "markdownlint-cli": "0.41.0",
         "postcss-html": "1.7.0",
-        "stylelint": "16.5.0",
+        "stylelint": "16.6.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.3.2",
-        "updates": "16.0.1",
+        "updates": "16.1.1",
         "vite-string-plugin": "1.3.1",
         "vitest": "1.6.0"
       },
@@ -121,11 +121,11 @@
       }
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.24.2",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
-      "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
+      "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==",
       "dependencies": {
-        "@babel/highlight": "^7.24.2",
+        "@babel/highlight": "^7.24.6",
         "picocolors": "^1.0.0"
       },
       "engines": {
@@ -133,19 +133,19 @@
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
-      "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz",
+      "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==",
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/highlight": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz",
-      "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz",
+      "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.5",
+        "@babel/helper-validator-identifier": "^7.24.6",
         "chalk": "^2.4.2",
         "js-tokens": "^4.0.0",
         "picocolors": "^1.0.0"
@@ -224,9 +224,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
-      "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz",
+      "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -235,9 +235,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
-      "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz",
+      "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -471,9 +471,9 @@
       }
     },
     "node_modules/@csstools/selector-specificity": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz",
-      "integrity": "sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz",
+      "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==",
       "funding": [
         {
           "type": "github",
@@ -1375,12 +1375,12 @@
       }
     },
     "node_modules/@playwright/test": {
-      "version": "1.44.0",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
-      "integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
+      "version": "1.44.1",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
+      "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
       "dev": true,
       "dependencies": {
-        "playwright": "1.44.0"
+        "playwright": "1.44.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -1451,9 +1451,9 @@
       "dev": true
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz",
-      "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
+      "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==",
       "cpu": [
         "arm"
       ],
@@ -1464,9 +1464,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz",
-      "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz",
+      "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==",
       "cpu": [
         "arm64"
       ],
@@ -1477,9 +1477,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz",
-      "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz",
+      "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==",
       "cpu": [
         "arm64"
       ],
@@ -1490,9 +1490,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz",
-      "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz",
+      "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==",
       "cpu": [
         "x64"
       ],
@@ -1503,9 +1503,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz",
-      "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz",
+      "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==",
       "cpu": [
         "arm"
       ],
@@ -1516,9 +1516,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz",
-      "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz",
+      "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==",
       "cpu": [
         "arm"
       ],
@@ -1529,9 +1529,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz",
-      "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz",
+      "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==",
       "cpu": [
         "arm64"
       ],
@@ -1542,9 +1542,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz",
-      "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz",
+      "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==",
       "cpu": [
         "arm64"
       ],
@@ -1555,9 +1555,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz",
-      "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz",
+      "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==",
       "cpu": [
         "ppc64"
       ],
@@ -1568,9 +1568,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz",
-      "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz",
+      "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==",
       "cpu": [
         "riscv64"
       ],
@@ -1581,9 +1581,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz",
-      "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz",
+      "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==",
       "cpu": [
         "s390x"
       ],
@@ -1594,9 +1594,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz",
-      "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
+      "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
       "cpu": [
         "x64"
       ],
@@ -1607,9 +1607,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz",
-      "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz",
+      "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==",
       "cpu": [
         "x64"
       ],
@@ -1620,9 +1620,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz",
-      "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz",
+      "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==",
       "cpu": [
         "arm64"
       ],
@@ -1633,9 +1633,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz",
-      "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz",
+      "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==",
       "cpu": [
         "ia32"
       ],
@@ -1646,9 +1646,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz",
-      "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz",
+      "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==",
       "cpu": [
         "x64"
       ],
@@ -2324,9 +2324,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.12.11",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
-      "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
+      "version": "20.12.12",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
+      "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -2343,12 +2343,6 @@
       "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==",
       "dev": true
     },
-    "node_modules/@types/semver": {
-      "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": {
       "version": "0.23.9",
       "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@@ -2369,21 +2363,19 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
-      "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz",
+      "integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "7.8.0",
-        "@typescript-eslint/type-utils": "7.8.0",
-        "@typescript-eslint/utils": "7.8.0",
-        "@typescript-eslint/visitor-keys": "7.8.0",
-        "debug": "^4.3.4",
+        "@typescript-eslint/scope-manager": "7.10.0",
+        "@typescript-eslint/type-utils": "7.10.0",
+        "@typescript-eslint/utils": "7.10.0",
+        "@typescript-eslint/visitor-keys": "7.10.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
-        "semver": "^7.6.0",
         "ts-api-utils": "^1.3.0"
       },
       "engines": {
@@ -2404,15 +2396,15 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz",
-      "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.10.0.tgz",
+      "integrity": "sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.8.0",
-        "@typescript-eslint/types": "7.8.0",
-        "@typescript-eslint/typescript-estree": "7.8.0",
-        "@typescript-eslint/visitor-keys": "7.8.0",
+        "@typescript-eslint/scope-manager": "7.10.0",
+        "@typescript-eslint/types": "7.10.0",
+        "@typescript-eslint/typescript-estree": "7.10.0",
+        "@typescript-eslint/visitor-keys": "7.10.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2432,13 +2424,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz",
-      "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz",
+      "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.8.0",
-        "@typescript-eslint/visitor-keys": "7.8.0"
+        "@typescript-eslint/types": "7.10.0",
+        "@typescript-eslint/visitor-keys": "7.10.0"
       },
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -2449,13 +2441,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz",
-      "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz",
+      "integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.8.0",
-        "@typescript-eslint/utils": "7.8.0",
+        "@typescript-eslint/typescript-estree": "7.10.0",
+        "@typescript-eslint/utils": "7.10.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
       },
@@ -2476,9 +2468,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
-      "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz",
+      "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==",
       "dev": true,
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -2489,13 +2481,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
-      "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz",
+      "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.8.0",
-        "@typescript-eslint/visitor-keys": "7.8.0",
+        "@typescript-eslint/types": "7.10.0",
+        "@typescript-eslint/visitor-keys": "7.10.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -2517,18 +2509,15 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz",
-      "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz",
+      "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@types/json-schema": "^7.0.15",
-        "@types/semver": "^7.5.8",
-        "@typescript-eslint/scope-manager": "7.8.0",
-        "@typescript-eslint/types": "7.8.0",
-        "@typescript-eslint/typescript-estree": "7.8.0",
-        "semver": "^7.6.0"
+        "@typescript-eslint/scope-manager": "7.10.0",
+        "@typescript-eslint/types": "7.10.0",
+        "@typescript-eslint/typescript-estree": "7.10.0"
       },
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -2542,12 +2531,12 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
-      "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
+      "version": "7.10.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz",
+      "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.8.0",
+        "@typescript-eslint/types": "7.10.0",
         "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
@@ -3053,9 +3042,9 @@
       }
     },
     "node_modules/ajv": {
-      "version": "8.13.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz",
-      "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz",
+      "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==",
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
         "json-schema-traverse": "^1.0.0",
@@ -3480,11 +3469,11 @@
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
@@ -3612,9 +3601,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001617",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz",
-      "integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==",
+      "version": "1.0.30001623",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001623.tgz",
+      "integrity": "sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA==",
       "funding": [
         {
           "type": "opencollective",
@@ -3673,9 +3662,9 @@
       }
     },
     "node_modules/chart.js": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
-      "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz",
+      "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==",
       "dependencies": {
         "@kurkle/color": "^0.3.0"
       },
@@ -3933,9 +3922,9 @@
       "dev": true
     },
     "node_modules/core-js-compat": {
-      "version": "3.37.0",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.0.tgz",
-      "integrity": "sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==",
+      "version": "3.37.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
+      "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
       "dev": true,
       "dependencies": {
         "browserslist": "^4.23.0"
@@ -4012,9 +4001,9 @@
       }
     },
     "node_modules/css-loader": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.1.tgz",
-      "integrity": "sha512-OxIR5P2mjO1PSXk44bWuQ8XtMK4dpEqpIyERCx3ewOo3I8EmbcxMPUc5ScLtQfgXtOojoMv57So4V/C02HQLsw==",
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
+      "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==",
       "dependencies": {
         "icss-utils": "^5.1.0",
         "postcss": "^8.4.33",
@@ -4860,9 +4849,9 @@
       }
     },
     "node_modules/dompurify": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.2.tgz",
-      "integrity": "sha512-hLGGBI1tw5N8qTELr3blKjAML/LY4ANxksbS612UiJyDfyf/2D092Pvm+S7pmeTGJRqvlJkFzBoHBQKgQlOQVg=="
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.4.tgz",
+      "integrity": "sha512-2gnshi6OshmuKil8rMZuQCGiUF3cUxHY3NGDzUAdUx/NPEe5DVnO8BDoAQouvgwnx0R/+a6jUn36Z0FSdq8vww=="
     },
     "node_modules/domutils": {
       "version": "3.1.0",
@@ -4905,9 +4894,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.762",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.762.tgz",
-      "integrity": "sha512-rrFvGweLxPwwSwJOjIopy3Vr+J3cIPtZzuc74bmlvmBIgQO3VYJDvVrlj94iKZ3ukXUH64Ex31hSfRTLqvjYJQ=="
+      "version": "1.4.783",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz",
+      "integrity": "sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ=="
     },
     "node_modules/elkjs": {
       "version": "0.9.3",
@@ -5106,9 +5095,9 @@
       }
     },
     "node_modules/es-module-lexer": {
-      "version": "1.5.2",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz",
-      "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA=="
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz",
+      "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg=="
     },
     "node_modules/es-object-atoms": {
       "version": "1.0.0",
@@ -5443,9 +5432,9 @@
       }
     },
     "node_modules/eslint-plugin-github": {
-      "version": "4.10.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.2.tgz",
-      "integrity": "sha512-F1F5aAFgi1Y5hYoTFzGQACBkw5W1hu2Fu5FSTrMlXqrojJnKl1S2pWO/rprlowRQpt+hzHhqSpsfnodJEVd5QA==",
+      "version": "5.0.0-2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.0.0-2.tgz",
+      "integrity": "sha512-oQUFAF1wMBvRMGLvGWxVhZ46JNjKbPuuDufmUDZ3ZYyovWHCqqR5HLHTpTfmZQcyEXmjv9TWdsgfdMlod2fGMQ==",
       "dev": true,
       "dependencies": {
         "@github/browserslist-config": "^1.0.0",
@@ -5737,9 +5726,9 @@
       }
     },
     "node_modules/eslint-plugin-regexp": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.5.0.tgz",
-      "integrity": "sha512-I7vKcP0o75WS5SHiVNXN+Eshq49sbrweMQIuqSL3AId9AwDe9Dhbfug65vw64LxmOd4v+yf5l5Xt41y9puiq0g==",
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.6.0.tgz",
+      "integrity": "sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
@@ -5803,9 +5792,9 @@
       }
     },
     "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
-      "integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
       "dev": true,
       "dependencies": {
         "ajv": "^6.12.4",
@@ -6294,9 +6283,9 @@
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dependencies": {
         "to-regex-range": "^5.0.1"
       },
@@ -6562,6 +6551,7 @@
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -6751,9 +6741,9 @@
       }
     },
     "node_modules/happy-dom": {
-      "version": "14.10.1",
-      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.10.1.tgz",
-      "integrity": "sha512-GRbrZYIezi8+tTtffF4v2QcF8bk1h2loUTO5VYQz3GZdrL08Vk0fI+bwf/vFEBf4C/qVf/easLJ/MY1wwdhytA==",
+      "version": "14.11.1",
+      "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.11.1.tgz",
+      "integrity": "sha512-JuaGMxD3QlQei6LdAM9mMY9am/cHa978uFbkOpjN5x83DG+QQp/NLyVV4Ru7KOjs70XYZ4KbI0TNiO81nM7uQQ==",
       "dev": true,
       "dependencies": {
         "entities": "^4.5.0",
@@ -7028,6 +7018,7 @@
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
       "dependencies": {
         "once": "^1.3.0",
         "wrappy": "1"
@@ -7039,9 +7030,9 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     "node_modules/ini": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz",
-      "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
+      "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
       "dev": true,
       "engines": {
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
@@ -7575,9 +7566,9 @@
       }
     },
     "node_modules/jackspeak": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
-      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz",
+      "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==",
       "dependencies": {
         "@isaacs/cliui": "^8.0.2"
       },
@@ -7828,15 +7819,15 @@
       }
     },
     "node_modules/known-css-properties": {
-      "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==",
+      "version": "0.31.0",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.31.0.tgz",
+      "integrity": "sha512-sBPIUGTNF0czz0mwGGUoKKJC8Q7On1GPbCSFPfyEsfHb2DyBG0Y4QtV+EVWpINSaiGKZblDNuF5AezxSgOhesQ==",
       "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==",
+      "version": "0.3.23",
+      "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+      "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
       "dev": true
     },
     "node_modules/language-tags": {
@@ -8162,14 +8153,14 @@
       }
     },
     "node_modules/markdownlint-cli": {
-      "version": "0.40.0",
-      "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.40.0.tgz",
-      "integrity": "sha512-JXhI3dRQcaqwiFYpPz6VJ7aKYheD53GmTz9y4D/d0F1MbZDGOp9pqKlbOfUX/pHP/iAoeiE4wYRmk8/kjLakxA==",
+      "version": "0.41.0",
+      "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.41.0.tgz",
+      "integrity": "sha512-kp29tKrMKdn+xonfefjp3a/MsNzAd9c5ke0ydMEI9PR98bOjzglYN4nfMSaIs69msUf1DNkgevAIAPtK2SeX0Q==",
       "dev": true,
       "dependencies": {
-        "commander": "~12.0.0",
+        "commander": "~12.1.0",
         "get-stdin": "~9.0.0",
-        "glob": "~10.3.12",
+        "glob": "~10.4.1",
         "ignore": "~5.3.1",
         "js-yaml": "^4.1.0",
         "jsonc-parser": "~3.2.1",
@@ -8177,7 +8168,7 @@
         "markdownlint": "~0.34.0",
         "minimatch": "~9.0.4",
         "run-con": "~1.3.2",
-        "toml": "~3.0.0"
+        "smol-toml": "~1.2.0"
       },
       "bin": {
         "markdownlint": "markdownlint.js"
@@ -8187,31 +8178,31 @@
       }
     },
     "node_modules/markdownlint-cli/node_modules/commander": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
-      "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==",
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
       "dev": true,
       "engines": {
         "node": ">=18"
       }
     },
     "node_modules/markdownlint-cli/node_modules/glob": {
-      "version": "10.3.14",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.14.tgz",
-      "integrity": "sha512-4fkAqu93xe9Mk7le9v0y3VrPDqLKHarNi2s4Pv7f2yOvfhWfhc7hRPHC/JyqMqb8B/Dt/eGS4n7ykwf3fOsl8g==",
+      "version": "10.4.1",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
+      "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
       "dev": true,
       "dependencies": {
         "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.6",
-        "minimatch": "^9.0.1",
-        "minipass": "^7.0.4",
-        "path-scurry": "^1.11.0"
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "path-scurry": "^1.11.1"
       },
       "bin": {
         "glob": "dist/esm/bin.mjs"
       },
       "engines": {
-        "node": ">=16 || 14 >=14.17"
+        "node": ">=16 || 14 >=14.18"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -8329,9 +8320,9 @@
       }
     },
     "node_modules/mermaid": {
-      "version": "10.9.0",
-      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz",
-      "integrity": "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==",
+      "version": "10.9.1",
+      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz",
+      "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==",
       "dependencies": {
         "@braintree/sanitize-url": "^6.0.1",
         "@types/d3-scale": "^4.0.3",
@@ -8777,11 +8768,11 @@
       ]
     },
     "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
+      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
       "dependencies": {
-        "braces": "^3.0.2",
+        "braces": "^3.0.3",
         "picomatch": "^2.3.1"
       },
       "engines": {
@@ -8871,9 +8862,9 @@
       }
     },
     "node_modules/minipass": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz",
-      "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==",
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
       "engines": {
         "node": ">=16 || 14 >=14.17"
       }
@@ -8891,9 +8882,9 @@
       }
     },
     "node_modules/monaco-editor": {
-      "version": "0.48.0",
-      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.48.0.tgz",
-      "integrity": "sha512-goSDElNqFfw7iDHMg8WDATkfcyeLTNpBHQpO8incK6p5qZt5G/1j41X0xdGzpIkGojGXM+QiRQyLjnfDVvrpwA=="
+      "version": "0.49.0",
+      "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz",
+      "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ=="
     },
     "node_modules/monaco-editor-webpack-plugin": {
       "version": "7.1.0",
@@ -9362,15 +9353,15 @@
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
     },
     "node_modules/path-scurry": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.0.tgz",
-      "integrity": "sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw==",
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
       "dependencies": {
         "lru-cache": "^10.2.0",
         "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
       },
       "engines": {
-        "node": ">=16 || 14 >=14.17"
+        "node": ">=16 || 14 >=14.18"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -9406,9 +9397,9 @@
       "integrity": "sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA=="
     },
     "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
     },
     "node_modules/picomatch": {
       "version": "2.3.1",
@@ -9508,12 +9499,12 @@
       }
     },
     "node_modules/playwright": {
-      "version": "1.44.0",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
-      "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
+      "version": "1.44.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
+      "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
       "dev": true,
       "dependencies": {
-        "playwright-core": "1.44.0"
+        "playwright-core": "1.44.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -9526,9 +9517,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.44.0",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
-      "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
+      "version": "1.44.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
+      "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
       "dev": true,
       "bin": {
         "playwright-core": "cli.js"
@@ -9744,9 +9735,9 @@
       }
     },
     "node_modules/postcss-nesting": {
-      "version": "12.1.2",
-      "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.2.tgz",
-      "integrity": "sha512-FUmTHGDNundodutB4PUBxt/EPuhgtpk8FJGRsBhOuy+6FnkR2A8RZWIsyyy6XmhvX2DZQQWIkvu+HB4IbJm+Ew==",
+      "version": "12.1.5",
+      "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.5.tgz",
+      "integrity": "sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==",
       "funding": [
         {
           "type": "github",
@@ -9759,8 +9750,8 @@
       ],
       "dependencies": {
         "@csstools/selector-resolve-nested": "^1.1.0",
-        "@csstools/selector-specificity": "^3.0.3",
-        "postcss-selector-parser": "^6.0.13"
+        "@csstools/selector-specificity": "^3.1.1",
+        "postcss-selector-parser": "^6.1.0"
       },
       "engines": {
         "node": "^14 || ^16 || >=18"
@@ -9818,9 +9809,9 @@
       }
     },
     "node_modules/postcss-selector-parser": {
-      "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==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
+      "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -10301,6 +10292,7 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
       "dev": true,
       "dependencies": {
         "glob": "^7.1.3"
@@ -10508,17 +10500,17 @@
       }
     },
     "node_modules/seroval": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.5.tgz",
-      "integrity": "sha512-TM+Z11tHHvQVQKeNlOUonOWnsNM+2IBwZ4vwoi4j3zKzIpc5IDw8WPwCfcc8F17wy6cBcJGbZbFOR0UCuTZHQA==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.7.tgz",
+      "integrity": "sha512-n6ZMQX5q0Vn19Zq7CIKNIo7E75gPkGCFUEqDpa8jgwpYr/vScjqnQ6H09t1uIiZ0ZSK0ypEGvrYK2bhBGWsGdw==",
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/seroval-plugins": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.5.tgz",
-      "integrity": "sha512-8+pDC1vOedPXjKG7oz8o+iiHrtF2WswaMQJ7CKFpccvSYfrzmvKY9zOJWCg+881722wIHfwkdnRmiiDm9ym+zQ==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.7.tgz",
+      "integrity": "sha512-GO7TkWvodGp6buMEX9p7tNyIkbwlyuAWbI6G9Ec5bhcm7mQdu3JOK1IXbEUwb3FVzSc363GraG/wLW23NSavIw==",
       "engines": {
         "node": ">=10"
       },
@@ -10661,6 +10653,16 @@
         "url": "https://github.com/chalk/slice-ansi?sponsor=1"
       }
     },
+    "node_modules/smol-toml": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.2.0.tgz",
+      "integrity": "sha512-KObxdQANC/xje3OoatMbSwQf2XAvJ0RbK+4nmQRszFNZptbNRnMWqbLF/zb4sMi9xJ6HNyhWXeuZ9zC/I/XY7w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 18",
+        "pnpm": ">= 9"
+      }
+    },
     "node_modules/solid-js": {
       "version": "1.8.17",
       "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.17.tgz",
@@ -10767,9 +10769,9 @@
       }
     },
     "node_modules/spdx-license-ids": {
-      "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=="
+      "version": "3.0.18",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz",
+      "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ=="
     },
     "node_modules/spdx-ranges": {
       "version": "2.1.1",
@@ -10981,16 +10983,26 @@
       "dev": true
     },
     "node_modules/stylelint": {
-      "version": "16.5.0",
-      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.5.0.tgz",
-      "integrity": "sha512-IlCBtVrG+qTy3v+tZTk50W8BIomjY/RUuzdrDqdnlCYwVuzXtPbiGfxYqtyYAyOMcb+195zRsuHn6tgfPmFfbw==",
+      "version": "16.6.0",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.6.0.tgz",
+      "integrity": "sha512-vjWYlDEgOS3Z/IcXagQwi8PFJyPro1DxBYOnTML1PAqnrYUHs8owleGStv20sgt0OhW8r9zZm6MK7IT2+l2B6A==",
       "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/stylelint"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/stylelint"
+        }
+      ],
       "dependencies": {
-        "@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.3",
-        "@dual-bundle/import-meta-resolve": "^4.0.0",
+        "@csstools/css-parser-algorithms": "^2.6.3",
+        "@csstools/css-tokenizer": "^2.3.1",
+        "@csstools/media-query-list-parser": "^2.1.11",
+        "@csstools/selector-specificity": "^3.1.1",
+        "@dual-bundle/import-meta-resolve": "^4.1.0",
         "balanced-match": "^2.0.0",
         "colord": "^2.9.3",
         "cosmiconfig": "^9.0.0",
@@ -11007,16 +11019,16 @@
         "ignore": "^5.3.1",
         "imurmurhash": "^0.1.4",
         "is-plain-object": "^5.0.0",
-        "known-css-properties": "^0.30.0",
+        "known-css-properties": "^0.31.0",
         "mathml-tag-names": "^2.1.3",
         "meow": "^13.2.0",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
-        "picocolors": "^1.0.0",
+        "picocolors": "^1.0.1",
         "postcss": "^8.4.38",
         "postcss-resolve-nested-selector": "^0.1.1",
         "postcss-safe-parser": "^7.0.0",
-        "postcss-selector-parser": "^6.0.16",
+        "postcss-selector-parser": "^6.1.0",
         "postcss-value-parser": "^4.2.0",
         "resolve-from": "^5.0.0",
         "string-width": "^4.2.3",
@@ -11031,10 +11043,6 @@
       },
       "engines": {
         "node": ">=18.12.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/stylelint"
       }
     },
     "node_modules/stylelint-declaration-block-no-ignored-properties": {
@@ -11234,21 +11242,21 @@
       }
     },
     "node_modules/sucrase/node_modules/glob": {
-      "version": "10.3.14",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.14.tgz",
-      "integrity": "sha512-4fkAqu93xe9Mk7le9v0y3VrPDqLKHarNi2s4Pv7f2yOvfhWfhc7hRPHC/JyqMqb8B/Dt/eGS4n7ykwf3fOsl8g==",
+      "version": "10.4.1",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
+      "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
       "dependencies": {
         "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.6",
-        "minimatch": "^9.0.1",
-        "minipass": "^7.0.4",
-        "path-scurry": "^1.11.0"
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "path-scurry": "^1.11.1"
       },
       "bin": {
         "glob": "dist/esm/bin.mjs"
       },
       "engines": {
-        "node": ">=16 || 14 >=14.17"
+        "node": ">=16 || 14 >=14.18"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -11345,9 +11353,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "5.17.7",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.7.tgz",
-      "integrity": "sha512-hKnq2Dss6Nvqxzj+tToBz0IJvKXgp7FExxX0Zj0rMajXJp8CJ98yLAwbKwKu8rxQf+2iIDUTGir84SCA8AN+fQ=="
+      "version": "5.17.13",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.13.tgz",
+      "integrity": "sha512-dyR3HAjwjK9oTd5ELzFh7rJEoMUyqfgaAQEwn0NGhLpOwg7IEbee17qjp50QIVE3sUA8J2d6ySw4IF50nUrKog=="
     },
     "node_modules/sync-fetch": {
       "version": "0.4.5",
@@ -11681,12 +11689,6 @@
       "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
       "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ=="
     },
-    "node_modules/toml": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
-      "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
-      "dev": true
-    },
     "node_modules/tr46": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -11936,9 +11938,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.15",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz",
-      "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==",
+      "version": "1.0.16",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",
+      "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -11955,7 +11957,7 @@
       ],
       "dependencies": {
         "escalade": "^3.1.2",
-        "picocolors": "^1.0.0"
+        "picocolors": "^1.0.1"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
@@ -11965,9 +11967,9 @@
       }
     },
     "node_modules/updates": {
-      "version": "16.0.1",
-      "resolved": "https://registry.npmjs.org/updates/-/updates-16.0.1.tgz",
-      "integrity": "sha512-If3NQKzGcA3aVgz2VyOXqQ+4uqYjPUPqh2PeZPtD+OKT4CTmxRYqoyFO+T3nwfccy4SiWy5AabWrBXXhVQ89Aw==",
+      "version": "16.1.1",
+      "resolved": "https://registry.npmjs.org/updates/-/updates-16.1.1.tgz",
+      "integrity": "sha512-h0Qtbmd9RCi6+99D5o7ACq4h7GxdYjeHFlxd4s0iO3lUOUDo1VnOsbNNIyjHpieVEctaEm/zoEjVggCgAcO/vg==",
       "dev": true,
       "bin": {
         "updates": "dist/updates.js"
@@ -12161,9 +12163,9 @@
       }
     },
     "node_modules/vite/node_modules/rollup": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
-      "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
+      "version": "4.18.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
+      "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
       "dev": true,
       "dependencies": {
         "@types/estree": "1.0.5"
@@ -12176,22 +12178,22 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.17.2",
-        "@rollup/rollup-android-arm64": "4.17.2",
-        "@rollup/rollup-darwin-arm64": "4.17.2",
-        "@rollup/rollup-darwin-x64": "4.17.2",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.17.2",
-        "@rollup/rollup-linux-arm-musleabihf": "4.17.2",
-        "@rollup/rollup-linux-arm64-gnu": "4.17.2",
-        "@rollup/rollup-linux-arm64-musl": "4.17.2",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2",
-        "@rollup/rollup-linux-riscv64-gnu": "4.17.2",
-        "@rollup/rollup-linux-s390x-gnu": "4.17.2",
-        "@rollup/rollup-linux-x64-gnu": "4.17.2",
-        "@rollup/rollup-linux-x64-musl": "4.17.2",
-        "@rollup/rollup-win32-arm64-msvc": "4.17.2",
-        "@rollup/rollup-win32-ia32-msvc": "4.17.2",
-        "@rollup/rollup-win32-x64-msvc": "4.17.2",
+        "@rollup/rollup-android-arm-eabi": "4.18.0",
+        "@rollup/rollup-android-arm64": "4.18.0",
+        "@rollup/rollup-darwin-arm64": "4.18.0",
+        "@rollup/rollup-darwin-x64": "4.18.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.18.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.18.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.18.0",
+        "@rollup/rollup-linux-arm64-musl": "4.18.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.18.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.18.0",
+        "@rollup/rollup-linux-x64-gnu": "4.18.0",
+        "@rollup/rollup-linux-x64-musl": "4.18.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.18.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.18.0",
+        "@rollup/rollup-win32-x64-msvc": "4.18.0",
         "fsevents": "~2.3.2"
       }
     },
diff --git a/package.json b/package.json
index d0de1efd5a..d7588e093f 100644
--- a/package.json
+++ b/package.json
@@ -17,11 +17,11 @@
     "add-asset-webpack-plugin": "3.0.0",
     "ansi_up": "6.0.2",
     "asciinema-player": "3.7.1",
-    "chart.js": "4.4.2",
+    "chart.js": "4.4.3",
     "chartjs-adapter-dayjs-4": "1.0.4",
     "chartjs-plugin-zoom": "2.0.1",
     "clippie": "4.1.1",
-    "css-loader": "7.1.1",
+    "css-loader": "7.1.2",
     "dayjs": "1.11.11",
     "dropzone": "6.0.0-beta.2",
     "easymde": "2.18.0",
@@ -33,17 +33,17 @@
     "jquery": "3.7.1",
     "katex": "0.16.10",
     "license-checker-webpack-plugin": "0.2.1",
-    "mermaid": "10.9.0",
+    "mermaid": "10.9.1",
     "mini-css-extract-plugin": "2.9.0",
     "minimatch": "9.0.4",
-    "monaco-editor": "0.48.0",
+    "monaco-editor": "0.49.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.2",
+    "postcss-nesting": "12.1.5",
     "sortablejs": "1.15.2",
-    "swagger-ui-dist": "5.17.7",
+    "swagger-ui-dist": "5.17.13",
     "tailwindcss": "3.4.3",
     "temporal-polyfill": "0.2.4",
     "throttle-debounce": "5.0.0",
@@ -63,19 +63,19 @@
   },
   "devDependencies": {
     "@eslint-community/eslint-plugin-eslint-comments": "4.3.0",
-    "@playwright/test": "1.44.0",
+    "@playwright/test": "1.44.1",
     "@stoplight/spectral-cli": "6.11.1",
     "@stylistic/eslint-plugin-js": "2.1.0",
     "@stylistic/stylelint-plugin": "2.1.2",
     "@vitejs/plugin-vue": "5.0.4",
     "eslint": "8.57.0",
     "eslint-plugin-array-func": "4.0.0",
-    "eslint-plugin-github": "4.10.2",
+    "eslint-plugin-github": "5.0.0-2",
     "eslint-plugin-i": "2.29.1",
     "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.5.0",
+    "eslint-plugin-regexp": "2.6.0",
     "eslint-plugin-sonarjs": "1.0.3",
     "eslint-plugin-unicorn": "53.0.0",
     "eslint-plugin-vitest": "0.4.1",
@@ -83,15 +83,15 @@
     "eslint-plugin-vue": "9.26.0",
     "eslint-plugin-vue-scoped-css": "2.8.0",
     "eslint-plugin-wc": "2.1.0",
-    "happy-dom": "14.10.1",
-    "markdownlint-cli": "0.40.0",
+    "happy-dom": "14.11.1",
+    "markdownlint-cli": "0.41.0",
     "postcss-html": "1.7.0",
-    "stylelint": "16.5.0",
+    "stylelint": "16.6.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.3.2",
-    "updates": "16.0.1",
+    "updates": "16.1.1",
     "vite-string-plugin": "1.3.1",
     "vitest": "1.6.0"
   },
diff --git a/updates.config.js b/updates.config.js
index bd072fe6cb..a4a2fa5228 100644
--- a/updates.config.js
+++ b/updates.config.js
@@ -3,6 +3,7 @@ export default {
     '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
     'eslint', // need to migrate to eslint flat config first
     'eslint-plugin-array-func', // need to migrate to eslint flat config first
+    'eslint-plugin-no-use-extend-native', // need to migrate to eslint flat config first
     'eslint-plugin-vitest', // need to migrate to eslint flat config first
   ],
 };

From 858d4f221d71e9d761048d302f04cba223d5d9da Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 28 May 2024 04:13:42 +0200
Subject: [PATCH 055/131] Fix DashboardRepoList margin (#31121)

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

<img width="476" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/ba508ba9-b02d-47c6-ad9f-495101c81330">
---
 web_src/js/components/DashboardRepoList.vue | 2 --
 1 file changed, 2 deletions(-)

diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 8bce40ee79..3f9f427cd7 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -509,10 +509,8 @@ ul li:not(:last-child) {
 }
 
 .repos-filter {
-  padding-top: 0 !important;
   margin-top: 0 !important;
   border-bottom-width: 0 !important;
-  margin-bottom: 2px !important;
 }
 
 .repos-filter .item {

From cd7d1314fc6598931e9a651a1c17026b28aa2c62 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 28 May 2024 10:43:13 +0800
Subject: [PATCH 056/131] Fix API repository object format missed (#31118)

Fix #31117
---
 services/convert/repository.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/services/convert/repository.go b/services/convert/repository.go
index 3b293fe550..26c591dd88 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -236,6 +236,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		MirrorInterval:                mirrorInterval,
 		MirrorUpdated:                 mirrorUpdated,
 		RepoTransfer:                  transfer,
+		ObjectFormatName:              repo.ObjectFormatName,
 	}
 }
 

From b6f15c7948ac3d09977350de83ec91d5789ea083 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 28 May 2024 17:31:59 +0800
Subject: [PATCH 057/131] Add missed return after `ctx.ServerError` (#31130)

---
 routers/api/v1/repo/mirror.go | 1 +
 routers/web/admin/repos.go    | 1 +
 routers/web/auth/auth.go      | 1 +
 routers/web/org/projects.go   | 1 +
 routers/web/repo/editor.go    | 1 +
 5 files changed, 5 insertions(+)

diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go
index 2a896de4fe..eddd449206 100644
--- a/routers/api/v1/repo/mirror.go
+++ b/routers/api/v1/repo/mirror.go
@@ -383,6 +383,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
 	if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
 		if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
 			ctx.ServerError("DeletePushMirrors", err)
+			return
 		}
 		ctx.ServerError("AddPushMirrorRemote", err)
 		return
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index 0815879bb3..e7c27145dc 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -95,6 +95,7 @@ func UnadoptedRepos(ctx *context.Context) {
 	repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, q, &opts)
 	if err != nil {
 		ctx.ServerError("ListUnadoptedRepositories", err)
+		return
 	}
 	ctx.Data["Dirs"] = repoNames
 	pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 4083d64226..842020791f 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -831,6 +831,7 @@ func ActivateEmail(ctx *context.Context) {
 	if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
 		if err := user_model.ActivateEmail(ctx, email); err != nil {
 			ctx.ServerError("ActivateEmail", err)
+			return
 		}
 
 		log.Trace("Email activated: %s", email.Email)
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 8fb8f2540f..9ab3c21cb2 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -569,6 +569,7 @@ func MoveIssues(ctx *context.Context) {
 	form := &movedIssuesForm{}
 	if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
 		ctx.ServerError("DecodeMovedIssuesForm", err)
+		return
 	}
 
 	issueIDs := make([]int64, 0, len(form.Issues))
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 474d7503e4..4ff86b5a66 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -562,6 +562,7 @@ func DeleteFilePost(ctx *context.Context) {
 		} else {
 			ctx.ServerError("DeleteRepoFile", err)
 		}
+		return
 	}
 
 	ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))

From de4616690f742aebc3e019fde5c73c432d543292 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 28 May 2024 18:03:54 +0800
Subject: [PATCH 058/131] Add topics for repository API (#31127)

Fix ##31100
---
 modules/structs/repo.go        | 1 +
 services/convert/repository.go | 1 +
 templates/swagger/v1_json.tmpl | 7 +++++++
 3 files changed, 9 insertions(+)

diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 1fe826cf89..444967c3e7 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -113,6 +113,7 @@ type Repository struct {
 	// swagger:strfmt date-time
 	MirrorUpdated time.Time     `json:"mirror_updated,omitempty"`
 	RepoTransfer  *RepoTransfer `json:"repo_transfer"`
+	Topics        []string      `json:"topics"`
 }
 
 // CreateRepoOption options when creating repository
diff --git a/services/convert/repository.go b/services/convert/repository.go
index 26c591dd88..d7568e8d08 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -236,6 +236,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		MirrorInterval:                mirrorInterval,
 		MirrorUpdated:                 mirrorUpdated,
 		RepoTransfer:                  transfer,
+		Topics:                        repo.Topics,
 		ObjectFormatName:              repo.ObjectFormatName,
 	}
 }
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 34829a15fc..c552e48346 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -23804,6 +23804,13 @@
           "type": "boolean",
           "x-go-name": "Template"
         },
+        "topics": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "Topics"
+        },
         "updated_at": {
           "type": "string",
           "format": "date-time",

From 1e3c4d8fc702aeedc359162ab1284b30a2a59717 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 28 May 2024 15:41:37 +0200
Subject: [PATCH 059/131] Improve mobile review ui (#31091)

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

Not perfect but much better than before.

Before: Overflows, sticky not working, filename unreadable:

<img width="506" alt="Screenshot 2024-05-27 at 02 02 40"
src="https://github.com/go-gitea/gitea/assets/115237/a06b1edf-dece-4402-98c2-68670fca265f">

After:
<img width="457" alt="Screenshot 2024-05-27 at 01 59 06"
src="https://github.com/go-gitea/gitea/assets/115237/2a282c96-e719-4554-b418-81963ae6269c">
---
 templates/repo/diff/box.tmpl          |  2 +-
 templates/repo/diff/conversation.tmpl |  8 +--
 web_src/css/markup/content.css        |  2 +-
 web_src/css/modules/comment.css       | 14 +++--
 web_src/css/modules/segment.css       |  3 +-
 web_src/css/repo.css                  | 82 ++++++++++++---------------
 web_src/css/review.css                | 70 +++++------------------
 7 files changed, 69 insertions(+), 112 deletions(-)

diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index daacdf4ba0..2f9d4ecab6 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}} 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">
+						<h4 class="diff-file-header sticky-2nd-row ui top attached header">
 							<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}}
diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl
index c263ddcdd6..08f60644b3 100644
--- a/templates/repo/diff/conversation.tmpl
+++ b/templates/repo/diff/conversation.tmpl
@@ -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 tw-mt-2">
-				<div class="ui buttons tw-mr-1">
+			<div class="tw-flex tw-justify-end tw-items-center tw-gap-2 tw-mt-2 tw-flex-wrap">
+				<div class="ui buttons">
 					<button class="ui icon tiny basic button previous-conversation">
 						{{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}}
 					</button>
@@ -50,7 +50,7 @@
 					</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">
+					<button class="ui icon tiny basic button resolve-conversation tw-mr-0" 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}}
@@ -59,7 +59,7 @@
 					</button>
 				{{end}}
 				{{if and $.SignedUserID (not $.Repository.IsArchived)}}
-					<button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0">
+					<button class="comment-form-reply ui primary tiny labeled icon button tw-mr-0">
 						{{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}}
 					</button>
 				{{end}}
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index 3eb40eaf29..9546c11d6a 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -2,7 +2,7 @@
   overflow: hidden;
   font-size: 16px;
   line-height: 1.5 !important;
-  word-wrap: break-word;
+  overflow-wrap: anywhere;
 }
 
 .markup > *:first-child {
diff --git a/web_src/css/modules/comment.css b/web_src/css/modules/comment.css
index cf080db225..672808e9cc 100644
--- a/web_src/css/modules/comment.css
+++ b/web_src/css/modules/comment.css
@@ -14,6 +14,7 @@
 }
 
 .ui.comments .comment {
+  display: flex;
   position: relative;
   background: none;
   margin: 3px 0 0;
@@ -23,6 +24,10 @@
   line-height: 1.2;
 }
 
+.edit-content-zone .comment {
+  flex-direction: column;
+}
+
 .ui.comments .comment:first-child {
   margin-top: 0;
   padding-top: 0;
@@ -46,16 +51,17 @@
 }
 
 .ui.comments .comment .avatar {
-  float: left;
-  width: 2.5em;
+  width: 30px;
 }
 
 .ui.comments .comment > .content {
-  display: block;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
 }
 
 .ui.comments .comment > .avatar ~ .content {
-  margin-left: 3.5em;
+  margin-left: 12px;
 }
 
 .ui.comments .comment .author {
diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css
index 48dc5c4488..0f555cea93 100644
--- a/web_src/css/modules/segment.css
+++ b/web_src/css/modules/segment.css
@@ -156,7 +156,8 @@
 .ui.attached.segment:last-child,
 .ui.segment:has(+ .ui.segment:not(.attached)),
 .ui.attached.segment:has(+ .ui.modal) {
-  border-radius: 0 0 0.28571429rem 0.28571429rem;
+  border-bottom-left-radius: 0.28571429rem;
+  border-bottom-right-radius: 0.28571429rem;
 }
 
 .ui[class*="top attached"].segment {
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index ce5d3c7951..d3036744fe 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -824,8 +824,7 @@ td .commit-summary {
   padding-top: 0;
 }
 
-.repository.view.issue .comment-list .timeline-item.commits-list .ui.avatar,
-.repository.view.issue .comment-list .timeline-item.event .ui.avatar {
+.repository.view.issue .comment-list .timeline-item.commits-list .ui.avatar {
   margin-right: 0.25em;
 }
 
@@ -1037,10 +1036,6 @@ td .commit-summary {
   margin-top: 6px;
 }
 
-.repository.view.issue .comment-list .comment > .avatar ~ .content {
-  margin-left: 42px;
-}
-
 .repository.view.issue .comment-list .comment-code-cloud button.comment-form-reply {
   margin: 0;
 }
@@ -1064,12 +1059,6 @@ td .commit-summary {
   box-shadow: none;
 }
 
-@media (max-width: 767.98px) {
-  .repository.view.issue .comment-list {
-    padding: 1rem 0 !important; /* Important is required here to override existing fomantic styles. */
-  }
-}
-
 .repository.view.issue .ui.depending .item.is-closed .title {
   text-decoration: line-through;
 }
@@ -1551,39 +1540,6 @@ td .commit-summary {
   height: 30px;
 }
 
-.repository .diff-box .header:not(.resolved-placeholder) {
-  display: flex;
-  align-items: center;
-}
-
-.repository .diff-box .header:not(.resolved-placeholder) .file {
-  min-width: 0;
-}
-
-.repository .diff-box .header:not(.resolved-placeholder) .file .file-link {
-  max-width: fit-content;
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-  -webkit-line-clamp: 2;
-  overflow: hidden;
-}
-
-.repository .diff-box .header:not(.resolved-placeholder) .button {
-  padding: 0 12px;
-  flex: 0 0 auto;
-  margin-right: 0;
-  height: 30px;
-}
-
-.repository .diff-box .resolved-placeholder {
-  display: flex;
-  align-items: center;
-  font-size: 14px !important;
-  height: 36px;
-  padding-top: 0;
-  padding-bottom: 0;
-}
-
 .repository .diff-box .resolved-placeholder .button {
   padding: 8px 12px;
 }
@@ -2428,6 +2384,10 @@ tbody.commit-list {
 }
 
 .resolved-placeholder {
+  display: flex;
+  align-items: center;
+  font-size: 14px !important;
+  padding: 8px !important;
   font-weight: var(--font-weight-normal) !important;
   border: 1px solid var(--color-secondary) !important;
   border-radius: var(--border-radius) !important;
@@ -2537,6 +2497,38 @@ tbody.commit-list {
 .diff-file-header {
   padding: 5px 8px !important;
   box-shadow: 0 -1px 0 1px var(--color-body); /* prevent borders being visible behind top corners when sticky and scrolled */
+  font-weight: var(--font-weight-normal);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.diff-file-header .file {
+  min-width: 0;
+}
+
+.diff-file-header .file-link {
+  max-width: fit-content;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+  overflow-wrap: anywhere;
+}
+
+.diff-file-header .button {
+  padding: 0 12px;
+  flex: 0 0 auto;
+  margin-right: 0;
+  height: 30px;
+}
+
+@media (max-width: 767.98px) {
+  .diff-file-header {
+    flex-direction: column;
+    align-items: stretch;
+  }
 }
 
 .diff-file-box[data-folded="true"] .diff-file-body {
diff --git a/web_src/css/review.css b/web_src/css/review.css
index 7534500e6f..6337748939 100644
--- a/web_src/css/review.css
+++ b/web_src/css/review.css
@@ -3,6 +3,7 @@
   -webkit-touch-callout: none;
   -webkit-user-select: none;
   user-select: none;
+  margin-right: 0 !important;
 }
 
 .ui.button.add-code-comment {
@@ -71,57 +72,10 @@
   max-width: 820px;
 }
 
-@media (max-width: 767.98px) {
-  .comment-code-cloud {
-    max-width: none;
-    padding: 0.75rem !important;
-  }
-  .comment-code-cloud .code-comment-buttons {
-    margin: 0.5rem 0 0.25rem !important;
-  }
-  .comment-code-cloud .code-comment-buttons .code-comment-buttons-buttons {
-    width: 100%;
-  }
-  .comment-code-cloud .ui.buttons {
-    width: 100%;
-    margin: 0 !important;
-  }
-  .comment-code-cloud .ui.buttons .button {
-    flex: 1;
-  }
-}
-
 .comment-code-cloud .comments .comment {
   padding: 0;
 }
 
-@media (max-width: 767.98px) {
-  .comment-code-cloud .comments .comment .comment-header-right.actions .ui.basic.label {
-    display: none;
-  }
-  .comment-code-cloud .comments .comment .avatar {
-    width: auto;
-    float: none;
-    margin: 0 0.5rem 0 0;
-    flex-shrink: 0;
-  }
-  .comment-code-cloud .comments .comment .avatar ~ .content {
-    margin-left: 1em;
-  }
-  .comment-code-cloud .comments .comment img.avatar {
-    margin: 0 !important;
-  }
-  .comment-code-cloud .comments .comment .comment-content {
-    margin-left: 0 !important;
-  }
-  .comment-code-cloud .comments .comment .comment-container {
-    width: 100%;
-  }
-  .comment-code-cloud .comments .comment.code-comment {
-    padding: 0 0 0.5rem !important;
-  }
-}
-
 .comment-code-cloud .attached.tab {
   border: 0;
   padding: 0;
@@ -132,6 +86,13 @@
   padding: 1px 8px 1px 12px;
 }
 
+@media (max-width: 767.98px) {
+  .comment-code-cloud .attached.header {
+    padding-top: 4px;
+    padding-bottom: 4px;
+  }
+}
+
 .comment-code-cloud .attached.header .text {
   margin: 0;
 }
@@ -179,14 +140,6 @@
   display: block;
 }
 
-@media (max-width: 767.98px) {
-  .comment-code-cloud .button {
-    width: 100%;
-    margin: 0 !important;
-    margin-bottom: 0.75rem !important;
-  }
-}
-
 .diff-file-body .comment-form {
   margin: 0 0 0 3em;
 }
@@ -273,11 +226,16 @@
   align-items: center;
   border: 1px solid transparent;
   padding: 4px 8px;
-  margin: -8px 0; /* just like other buttons in the diff box header */
   border-radius: var(--border-radius);
   font-size: 0.857rem; /* just like .ui.tiny.button */
 }
 
+@media (max-width: 767.98px) {
+  .viewed-file-form {
+    margin-left: auto;
+  }
+}
+
 .viewed-file-form input {
   margin-right: 4px;
 }

From 4fe415683e685838fde4e11f14f0309bbadb36e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= <Mic92@users.noreply.github.com>
Date: Tue, 28 May 2024 17:30:34 +0200
Subject: [PATCH 060/131] Add an immutable tarball link to archive download
 headers for Nix (#31139)

This allows `nix flake metadata` and nix in general to lock a *branch*
tarball link in a manner that causes it to fetch the correct commit even
if the branch is updated with a newer version.

Co-authored-by: Jade Lovelace <software@lfcode.ca>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/api/v1/repo/file.go                |  6 ++++++
 routers/web/repo/repo.go                   |  6 ++++++
 tests/integration/api_repo_archive_test.go | 11 +++++++++++
 3 files changed, 23 insertions(+)

diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 156033f58a..979f5f30b9 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -319,6 +319,12 @@ func archiveDownload(ctx *context.APIContext) {
 func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
 	downloadName := ctx.Repo.Repository.Name + "-" + archiveName
 
+	// Add nix format link header so tarballs lock correctly:
+	// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
+	ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`,
+		ctx.Repo.Repository.APIURL(),
+		archiver.CommitID, archiver.CommitID))
+
 	rPath := archiver.RelativePath()
 	if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
 		// If we have a signed url (S3, object storage), redirect to this directly.
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 71c582b5f9..f54b35c3e0 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -484,6 +484,12 @@ func Download(ctx *context.Context) {
 func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
 	downloadName := ctx.Repo.Repository.Name + "-" + archiveName
 
+	// Add nix format link header so tarballs lock correctly:
+	// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
+	ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`,
+		ctx.Repo.Repository.APIURL(),
+		archiver.CommitID, archiver.CommitID))
+
 	rPath := archiver.RelativePath()
 	if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
 		// If we have a signed url (S3, object storage), redirect to this directly.
diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go
index 57d3abfe84..eecb84d5d1 100644
--- a/tests/integration/api_repo_archive_test.go
+++ b/tests/integration/api_repo_archive_test.go
@@ -8,6 +8,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"regexp"
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
@@ -39,6 +40,16 @@ func TestAPIDownloadArchive(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, bs, 266)
 
+	// Must return a link to a commit ID as the "immutable" archive link
+	linkHeaderRe := regexp.MustCompile(`^<(https?://.*/api/v1/repos/user2/repo1/archive/[a-f0-9]+\.tar\.gz.*)>; rel="immutable"$`)
+	m := linkHeaderRe.FindStringSubmatch(resp.Header().Get("Link"))
+	assert.NotEmpty(t, m[1])
+	resp = MakeRequest(t, NewRequest(t, "GET", m[1]).AddTokenAuth(token), http.StatusOK)
+	bs2, err := io.ReadAll(resp.Body)
+	assert.NoError(t, err)
+	// The locked URL should give the same bytes as the non-locked one
+	assert.EqualValues(t, bs, bs2)
+
 	link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.bundle", user2.Name, repo.Name))
 	resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
 	bs, err = io.ReadAll(resp.Body)

From 207c0c6c928f67a0159783f9d1e31493097fcd70 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Wed, 29 May 2024 00:26:43 +0000
Subject: [PATCH 061/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_pt-PT.ini | 5 +++++
 options/locale/locale_tr-TR.ini | 3 +++
 2 files changed, 8 insertions(+)

diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 28f040e7cf..4c05d7410e 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1378,6 +1378,7 @@ commitstatus.success=Sucesso
 ext_issues=Acesso a questões externas
 ext_issues.desc=Ligação para um rastreador de questões externo.
 
+projects.desc=Gerir questões e integrações nos planeamentos.
 projects.description=Descrição (opcional)
 projects.description_placeholder=Descrição
 projects.create=Criar planeamento
@@ -1441,6 +1442,7 @@ 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.already_changed=Não foi possível guardar as modificações da questão. O conteúdo parece ter sido modificado por outro utilizador, entretanto. Refresque a página e tente editar de novo para evitar sobrepor as modificações dele.
 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
@@ -1756,6 +1758,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.edit.already_changed=Não foi possível guardar as modificações do pedido de integração. O conteúdo parece ter sido modificado por outro utilizador, entretanto. Refresque a página e tente editar de novo para evitar sobrepor as modificações dele.
 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
@@ -1901,6 +1904,7 @@ pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2
 
 pull.deleted_branch=(eliminado):%s
 
+comments.edit.already_changed=Não foi possível guardar as modificações do comentário. O conteúdo parece ter sido modificado por outro utilizador, entretanto. Refresque a página e tente editar de novo para evitar sobrepor as modificações dele.
 
 milestones.new=Nova etapa
 milestones.closed=Encerrada %s
@@ -3637,6 +3641,7 @@ 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.no_job=A sequência de trabalho tem que conter pelo menos um trabalho
 runs.actor=Interveniente
 runs.status=Estado
 runs.actors_no_select=Todos os intervenientes
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 1cb056f578..f1ef7bd648 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -1439,6 +1439,7 @@ issues.new.clear_assignees=Atamaları Temizle
 issues.new.no_assignees=Atanan Kişi Yok
 issues.new.no_reviewers=Değerlendirici yok
 issues.new.blocked_user=Konu oluşturulamıyor, depo sahibi tarafından engellenmişsiniz.
+issues.edit.already_changed=Konuya yapılan değişiklikler kaydedilemiyor. İçerik başka kullanıcı tarafından değiştirilmiş gözüküyor. Diğerlerinin değişikliklerinin üzerine yazmamak için lütfen sayfayı yenileyin ve tekrar düzenlemeye çalışın
 issues.edit.blocked_user=İçerik düzenlenemiyor, gönderen veya depo sahibi tarafından engellenmişsiniz.
 issues.choose.get_started=Başla
 issues.choose.open_external_link=Aç
@@ -1754,6 +1755,7 @@ compare.compare_head=karşılaştır
 pulls.desc=Değişiklik isteklerini ve kod incelemelerini etkinleştir.
 pulls.new=Yeni Değişiklik İsteği
 pulls.new.blocked_user=Değişiklik isteği oluşturulamıyor, depo sahibi tarafından engellenmişsiniz.
+pulls.edit.already_changed=Değişiklik isteğine yapılan değişiklikler kaydedilemiyor. İçerik başka kullanıcı tarafından değiştirilmiş gözüküyor. Diğerlerinin değişikliklerinin üzerine yazmamak için lütfen sayfayı yenileyin ve tekrar düzenlemeye çalışın
 pulls.view=Değişiklik İsteği Görüntüle
 pulls.compare_changes=Yeni Değişiklik İsteği
 pulls.allow_edits_from_maintainers=Bakımcıların düzenlemelerine izin ver
@@ -1899,6 +1901,7 @@ pulls.recently_pushed_new_branches=<strong>%[1]s</strong> dalına ittiniz %[2]s
 
 pull.deleted_branch=(silindi): %s
 
+comments.edit.already_changed=Yoruma yapılan değişiklikler kaydedilemiyor. İçerik başka kullanıcı tarafından değiştirilmiş gözüküyor. Diğerlerinin değişikliklerinin üzerine yazmamak için lütfen sayfayı yenileyin ve tekrar düzenlemeye çalışın
 
 milestones.new=Yeni Kilometre Taşı
 milestones.closed=Kapalı %s

From c93cbc991e99a937223844e072a054cf76e815ca Mon Sep 17 00:00:00 2001
From: Samuel FORESTIER <HorlogeSkynet@users.noreply.github.com>
Date: Wed, 29 May 2024 00:35:21 +0000
Subject: [PATCH 062/131] Remove duplicate `ProxyPreserveHost` in Apache httpd
 doc (#31143)

---

(fix up for #31003)
---
 docs/content/administration/reverse-proxies.en-us.md | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docs/content/administration/reverse-proxies.en-us.md b/docs/content/administration/reverse-proxies.en-us.md
index 5fbd0eb0b7..dff58c10eb 100644
--- a/docs/content/administration/reverse-proxies.en-us.md
+++ b/docs/content/administration/reverse-proxies.en-us.md
@@ -169,7 +169,6 @@ If you want Apache HTTPD to serve your Gitea instance, you can add the following
     ProxyRequests off
     AllowEncodedSlashes NoDecode
     ProxyPass / http://localhost:3000/ nocanon
-    ProxyPreserveHost On
     RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
 </VirtualHost>
 ```

From 7034efc7dc0e355c63b11f0f633216d489d254be Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 29 May 2024 08:08:45 +0200
Subject: [PATCH 063/131] Use vertical layout for multiple code expander
 buttons (#31122)

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

- Now it only does a single call to `GetExpandDirection` per line
instead of multiples.
- Exposed `data-expand-direction` to frontend so it can correctly size
the buttons (it's a pain to do in tables).

<img width="142" alt="Screenshot 2024-05-27 at 20 44 56"
src="https://github.com/go-gitea/gitea/assets/115237/8b0b45a6-8e50-4081-8822-5e0775d8d941">
<img width="142" alt="Screenshot 2024-05-27 at 20 44 51"
src="https://github.com/go-gitea/gitea/assets/115237/b7ba2c57-8f55-4e9f-9606-c96d16b77892">
<img width="132" alt="Screenshot 2024-05-27 at 20 44 46"
src="https://github.com/go-gitea/gitea/assets/115237/0e838fb8-5e8c-4250-9843-a68b88d5418b">
<img width="80" alt="Screenshot 2024-05-27 at 20 44 33"
src="https://github.com/go-gitea/gitea/assets/115237/da6c7f83-c160-4389-8ab2-889d0568cbe8">
<img width="80" alt="Screenshot 2024-05-27 at 20 44 26"
src="https://github.com/go-gitea/gitea/assets/115237/cdb490b2-5040-484a-92e5-46fc5e37c199">
<img width="78" alt="Screenshot 2024-05-27 at 20 44 20"
src="https://github.com/go-gitea/gitea/assets/115237/d2978ab0-764e-41ff-922c-25f8fe749f28">

Would backport as trivial enhancement.
---
 templates/repo/diff/blob_excerpt.tmpl    | 18 ++++++++++--------
 templates/repo/diff/section_split.tmpl   |  9 +++++----
 templates/repo/diff/section_unified.tmpl |  9 +++++----
 web_src/css/review.css                   |  5 +++++
 4 files changed, 25 insertions(+), 16 deletions(-)

diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl
index a80abe263f..2874ac6a55 100644
--- a/templates/repo/diff/blob_excerpt.tmpl
+++ b/templates/repo/diff/blob_excerpt.tmpl
@@ -2,19 +2,20 @@
 	{{range $k, $line := $.section.Lines}}
 	<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}} line-expanded">
 		{{if eq .GetType 4}}
+			{{$expandDirection := $line.GetExpandDirection}}
 			<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}">
-				<div class="tw-flex">
-				{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
+				<div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}">
+				{{if or (eq $expandDirection 3) (eq $expandDirection 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"}}
 					</button>
 				{{end}}
-				{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
+				{{if or (eq $expandDirection 3) (eq $expandDirection 4)}}
 					<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}}
+				{{if eq $expandDirection 2}}
 					<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>
@@ -48,19 +49,20 @@
 	{{range $k, $line := $.section.Lines}}
 	<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}} line-expanded">
 		{{if eq .GetType 4}}
+			{{$expandDirection := $line.GetExpandDirection}}
 			<td colspan="2" class="lines-num">
-				<div class="tw-flex">
-					{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
+				<div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}">
+					{{if or (eq $expandDirection 3) (eq $expandDirection 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"}}
 						</button>
 					{{end}}
-					{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}}
+					{{if or (eq $expandDirection 3) (eq $expandDirection 4)}}
 						<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}}
+					{{if eq $expandDirection 2}}
 						<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>
diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl
index 349f0c3dfc..37b42bcb37 100644
--- a/templates/repo/diff/section_split.tmpl
+++ b/templates/repo/diff/section_split.tmpl
@@ -16,19 +16,20 @@
 		{{if or (ne .GetType 2) (not $hasmatch)}}
 			<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}" data-line-type="{{.GetHTMLDiffLineType}}">
 				{{if eq .GetType 4}}
+					{{$expandDirection := $line.GetExpandDirection}}
 					<td class="lines-num lines-num-old">
-						<div class="tw-flex">
-						{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
+						<div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}">
+						{{if or (eq $expandDirection 3) (eq $expandDirection 5)}}
 							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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)}}
+						{{if or (eq $expandDirection 3) (eq $expandDirection 4)}}
 							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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}}
+						{{if eq $expandDirection 2}}
 							<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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>
diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl
index ec59f4d42e..708b333291 100644
--- a/templates/repo/diff/section_unified.tmpl
+++ b/templates/repo/diff/section_unified.tmpl
@@ -12,19 +12,20 @@
 		<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}" data-line-type="{{.GetHTMLDiffLineType}}">
 			{{if eq .GetType 4}}
 				{{if $.root.AfterCommitID}}
+					{{$expandDirection := $line.GetExpandDirection}}
 					<td colspan="2" class="lines-num">
-						<div class="tw-flex">
-							{{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}}
+						<div class="code-expander-buttons" data-expand-direction="{{$expandDirection}}">
+							{{if or (eq $expandDirection 3) (eq $expandDirection 5)}}
 								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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)}}
+							{{if or (eq $expandDirection 3) (eq $expandDirection 4)}}
 								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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}}
+							{{if eq $expandDirection 2}}
 								<button class="code-expander-button" hx-target="closest tr" hx-get="{{$blobExcerptRepoLink}}/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>
diff --git a/web_src/css/review.css b/web_src/css/review.css
index 6337748939..0d69e36681 100644
--- a/web_src/css/review.css
+++ b/web_src/css/review.css
@@ -164,6 +164,11 @@
   flex: 1;
 }
 
+/* expand direction 3 is both ways with two buttons */
+.code-expander-buttons[data-expand-direction="3"] .code-expander-button {
+  height: 18px;
+}
+
 .code-expander-button:hover {
   background: var(--color-primary);
   color: var(--color-primary-contrast);

From 5c1b550e00e9460078e00c41a32d206b260ef482 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 29 May 2024 14:43:02 +0800
Subject: [PATCH 064/131] Fix push multiple branches error with tests (#31151)

---
 services/repository/branch.go                 |  2 +-
 .../git_helper_for_declarative_test.go        | 18 ++++++++++
 tests/integration/git_push_test.go            | 35 +++++++++++++++++++
 3 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/services/repository/branch.go b/services/repository/branch.go
index e1d036a97c..869921bfbc 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -332,7 +332,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
 				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
+				continue
 			}
 
 			// if database have branches but not this branch, it means this is a new branch
diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go
index 77fe07128e..d1d935da4f 100644
--- a/tests/integration/git_helper_for_declarative_test.go
+++ b/tests/integration/git_helper_for_declarative_test.go
@@ -160,6 +160,24 @@ func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T
 	}
 }
 
+func doGitAddSomeCommits(dstPath, branch string) func(*testing.T) {
+	return func(t *testing.T) {
+		doGitCheckoutBranch(dstPath, branch)(t)
+
+		assert.NoError(t, os.WriteFile(filepath.Join(dstPath, fmt.Sprintf("file-%s.txt", branch)), []byte(fmt.Sprintf("file %s", branch)), 0o644))
+		assert.NoError(t, git.AddChanges(dstPath, true))
+		signature := git.Signature{
+			Email: "test@test.test",
+			Name:  "test",
+		}
+		assert.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{
+			Committer: &signature,
+			Author:    &signature,
+			Message:   fmt.Sprintf("update %s", branch),
+		}))
+	}
+}
+
 func doGitCreateBranch(dstPath, branch string) func(*testing.T) {
 	return func(t *testing.T) {
 		_, _, err := git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(branch).RunStdString(&git.RunOpts{Dir: dstPath})
diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go
index b37fb02444..da254fc88f 100644
--- a/tests/integration/git_push_test.go
+++ b/tests/integration/git_push_test.go
@@ -37,6 +37,41 @@ func testGitPush(t *testing.T, u *url.URL) {
 		})
 	})
 
+	t.Run("Push branches exists", func(t *testing.T) {
+		runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
+			for i := 0; i < 10; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				if i < 5 {
+					pushed = append(pushed, branchName)
+				}
+				doGitCreateBranch(gitPath, branchName)(t)
+			}
+			// only push master and the first 5 branches
+			pushed = append(pushed, "master")
+			args := append([]string{"origin"}, pushed...)
+			doGitPushTestRepository(gitPath, args...)(t)
+
+			pushed = pushed[:0]
+			// do some changes for the first 5 branches created above
+			for i := 0; i < 5; i++ {
+				branchName := fmt.Sprintf("branch-%d", i)
+				pushed = append(pushed, branchName)
+
+				doGitAddSomeCommits(gitPath, branchName)(t)
+			}
+
+			for i := 5; i < 10; i++ {
+				pushed = append(pushed, fmt.Sprintf("branch-%d", i))
+			}
+			pushed = append(pushed, "master")
+
+			// push all, so that master are not chagned
+			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++ {

From 31011f5cde15bc8f8b58a714c201e6865ce9fd6e Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Wed, 29 May 2024 11:54:17 -0400
Subject: [PATCH 065/131] Swap word order in Comment and Close (#31148)

Reduce accident closing of tickets only to re-open them right away. This
aligns the text on these buttons with what GitHub has.

Commit is authored by @LazyDodo, and was committed to the Blender fork
by @brechtvl

Background details:
https://projects.blender.org/infrastructure/gitea-custom/pulls/7

Co-authored-by: Ray Molenkamp <github@lazydodo.com>
---
 options/locale/locale_en-US.ini | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 772b11c2ba..539715b3f9 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1555,9 +1555,9 @@ issues.no_content = No description provided.
 issues.close = Close Issue
 issues.comment_pull_merged_at = merged commit %[1]s into %[2]s %[3]s
 issues.comment_manually_pull_merged_at = manually merged commit %[1]s into %[2]s %[3]s
-issues.close_comment_issue = Comment and Close
+issues.close_comment_issue = Close with Comment
 issues.reopen_issue = Reopen
-issues.reopen_comment_issue = Comment and Reopen
+issues.reopen_comment_issue = Reopen with Comment
 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>`

From 34daee6baf2e454e9a99bf2f03ed46011bf38d18 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 30 May 2024 00:28:55 +0800
Subject: [PATCH 066/131] Fix markup preview (#31158)

Fix #31157

After:

![image](https://github.com/go-gitea/gitea/assets/2114189/4d918cce-cd0d-4601-9c81-4b32df1b0b38)
---
 routers/web/web.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/routers/web/web.go b/routers/web/web.go
index 6a17c19821..5fb1ce0e80 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1125,6 +1125,9 @@ func registerRoutes(m *web.Route) {
 	// user/org home, including rss feeds
 	m.Get("/{username}/{reponame}", ignSignIn, context.RepoAssignment, context.RepoRef(), repo.SetEditorconfigIfExists, repo.Home)
 
+	// TODO: maybe it should relax the permission to allow "any access"
+	m.Post("/{username}/{reponame}/markup", ignSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki), web.Bind(structs.MarkupOption{}), misc.Markup)
+
 	m.Group("/{username}/{reponame}", func() {
 		m.Get("/find/*", repo.FindFiles)
 		m.Group("/tree-list", func() {
@@ -1236,8 +1239,6 @@ func registerRoutes(m *web.Route) {
 			m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommentReaction)
 		}, context.RepoMustNotBeArchived())
 
-		m.Post("/markup", web.Bind(structs.MarkupOption{}), misc.Markup)
-
 		m.Group("/labels", func() {
 			m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel)
 			m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)

From ce751761ce218a4a011ed5659718f9b62ed8bcad Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Thu, 30 May 2024 00:26:20 +0000
Subject: [PATCH 067/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_cs-CZ.ini | 2 --
 options/locale/locale_de-DE.ini | 2 --
 options/locale/locale_el-GR.ini | 2 --
 options/locale/locale_es-ES.ini | 2 --
 options/locale/locale_fa-IR.ini | 2 --
 options/locale/locale_fi-FI.ini | 2 --
 options/locale/locale_fr-FR.ini | 2 --
 options/locale/locale_hu-HU.ini | 2 --
 options/locale/locale_id-ID.ini | 2 --
 options/locale/locale_is-IS.ini | 2 --
 options/locale/locale_it-IT.ini | 2 --
 options/locale/locale_ja-JP.ini | 2 --
 options/locale/locale_ko-KR.ini | 2 --
 options/locale/locale_lv-LV.ini | 2 --
 options/locale/locale_nl-NL.ini | 2 --
 options/locale/locale_pl-PL.ini | 2 --
 options/locale/locale_pt-BR.ini | 2 --
 options/locale/locale_pt-PT.ini | 2 --
 options/locale/locale_ru-RU.ini | 2 --
 options/locale/locale_si-LK.ini | 2 --
 options/locale/locale_sv-SE.ini | 2 --
 options/locale/locale_tr-TR.ini | 2 --
 options/locale/locale_uk-UA.ini | 2 --
 options/locale/locale_zh-CN.ini | 7 +++++--
 options/locale/locale_zh-TW.ini | 2 --
 25 files changed, 5 insertions(+), 50 deletions(-)

diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 0c61e5d042..acc0d0bc27 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -1537,9 +1537,7 @@ 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
-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>`
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 8e1194cdd1..8b6296cfdc 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -1540,9 +1540,7 @@ issues.no_content=Keine Beschreibung angegeben.
 issues.close=Issue schließen
 issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged
 issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged
-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`
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index 74262ff38d..7e6e2ba2cd 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -1463,9 +1463,7 @@ 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
-issues.close_comment_issue=Σχόλιο και κλείσιμο
 issues.reopen_issue=Ανοίξτε ξανά
-issues.reopen_comment_issue=Σχόλιο και Άνοιγμα ξανά
 issues.create_comment=Προσθήκη Σχολίου
 issues.closed_at=`αυτό το ζήτημα έκλεισε <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`ξανά άνοιξε αυτό το ζήτημα <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 66273eb79a..8c0dc836fd 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -1456,9 +1456,7 @@ issues.no_content=No se ha proporcionado una descripción.
 issues.close=Cerrar Incidencia
 issues.comment_pull_merged_at=commit fusionado %[1]s en %[2]s %[3]s
 issues.comment_manually_pull_merged_at=commit manualmente fusionado %[1]s en %[2]s %[3]s
-issues.close_comment_issue=Comentar y cerrar
 issues.reopen_issue=Reabrir
-issues.reopen_comment_issue=Comentar y reabrir
 issues.create_comment=Comentar
 issues.closed_at=`cerró esta incidencia <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`reabrió esta incidencia <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 94e572f9b4..2cc770ef09 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -1120,9 +1120,7 @@ issues.context.quote_reply=پاسخ نقل و قول
 issues.context.reference_issue=مرجع در شماره جدید
 issues.context.edit=ویرایش
 issues.context.delete=حذف
-issues.close_comment_issue=ثبت دیدگاه و بستن
 issues.reopen_issue=بازگشایی
-issues.reopen_comment_issue=ثبت دیدگاه و بازگشایی
 issues.create_comment=دیدگاه
 issues.closed_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> این موضوع را بست`
 issues.reopened_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> این موضوع را دوباره باز کرد`
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index d854e74e61..be0f1f5b64 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -888,9 +888,7 @@ issues.context.quote_reply=Vastaa lainaamalla
 issues.context.reference_issue=Viittaa uudesa ongelmassa
 issues.context.edit=Muokkaa
 issues.context.delete=Poista
-issues.close_comment_issue=Kommentoi ja sulje
 issues.reopen_issue=Avaa uudelleen
-issues.reopen_comment_issue=Kommentoi ja avaa uudelleen
 issues.create_comment=Kommentoi
 issues.closed_at=`sulki tämän ongelman <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`uudelleenavasi tämän ongelman <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 0def8f81d1..9a1a756264 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1477,9 +1477,7 @@ issues.no_content=Sans contenu.
 issues.close=Fermer le ticket
 issues.comment_pull_merged_at=a fusionné la révision %[1]s dans %[2]s %[3]s
 issues.comment_manually_pull_merged_at=a fusionné manuellement la révision %[1]s dans %[2]s %[3]s
-issues.close_comment_issue=Commenter et Fermer
 issues.reopen_issue=Rouvrir
-issues.reopen_comment_issue=Commenter et Réouvrir
 issues.create_comment=Commenter
 issues.closed_at=`a fermé ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.`
 issues.reopened_at=`a réouvert ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.`
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 06eb31f308..617c7d10c0 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -826,9 +826,7 @@ issues.context.copy_link=Hivatkozás másolása
 issues.context.quote_reply=Válasz idézettel
 issues.context.edit=Szerkesztés
 issues.context.delete=Törlés
-issues.close_comment_issue=Hozzászólás és lezárás
 issues.reopen_issue=Újranyitás
-issues.reopen_comment_issue=Hozzászólás és újranyitás
 issues.create_comment=Hozzászólás
 issues.commit_ref_at=`hivatkozott erre a hibajegyre egy commit-ból <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.role.owner=Tulajdonos
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index a6bac362ab..cd3257dfde 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -705,9 +705,7 @@ issues.context.copy_link=Salin tautan
 issues.context.quote_reply=Kutip Balasan
 issues.context.edit=Sunting
 issues.context.delete=Hapus
-issues.close_comment_issue=Komentar dan Tutup
 issues.reopen_issue=Buka kembali
-issues.reopen_comment_issue=Komentar dan Buka Kembali
 issues.create_comment=Komentar
 issues.commit_ref_at=`merujuk masalah dari komit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.role.owner=Pemilik
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index f6becbf1c0..1ec4d990cf 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -791,9 +791,7 @@ issues.num_comments=%d ummæli
 issues.commented_at=`gerði ummæli <a href="#%s">%s</a>`
 issues.context.edit=Breyta
 issues.context.delete=Eyða
-issues.close_comment_issue=Senda ummæli og Loka
 issues.reopen_issue=Enduropna
-issues.reopen_comment_issue=Senda ummæli og Enduropna
 issues.create_comment=Senda Ummæli
 issues.closed_at=`lokaði þessu vandamáli <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`enduropnaði þetta vandamál <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index a32ae01868..1f9e4e6a8e 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -1213,9 +1213,7 @@ issues.context.quote_reply=Quota risposta
 issues.context.reference_issue=Fai riferimento in un nuovo problema
 issues.context.edit=Modifica
 issues.context.delete=Elimina
-issues.close_comment_issue=Commenta e Chiudi
 issues.reopen_issue=Riapri
-issues.reopen_comment_issue=Commenta e Riapri
 issues.create_comment=Commento
 issues.closed_at=`chiuso questo probleam <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`riaperto questo problema <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 07c1cbfe7e..89df5ac0b9 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -1552,9 +1552,7 @@ 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
-issues.close_comment_issue=コメントしてクローズ
 issues.reopen_issue=再オープンする
-issues.reopen_comment_issue=コメントして再オープン
 issues.create_comment=コメントする
 issues.comment.blocked_user=投稿者またはリポジトリのオーナーがあなたをブロックしているため、コメントの作成や編集はできません。
 issues.closed_at=`がイシューをクローズ <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index 054632e819..c5bf621c47 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -754,9 +754,7 @@ issues.commented_at=`코멘트됨, <a href="#%s">%s</a>`
 issues.delete_comment_confirm=이 댓글을 정말 삭제하시겠습니까?
 issues.context.edit=수정하기
 issues.context.delete=삭제
-issues.close_comment_issue=클로즈 및 코멘트
 issues.reopen_issue=다시 열기
-issues.reopen_comment_issue=다시 오픈 및 코멘트
 issues.create_comment=코멘트
 issues.commit_ref_at=` 커밋 <a id="%[1]s" href="#%[1]s">%[2]s</a>에서 이 이슈 언급`
 issues.role.owner=소유자
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index 8f9766b082..120d8f3407 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -1465,9 +1465,7 @@ 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
-issues.close_comment_issue=Komentēt un aizvērt
 issues.reopen_issue=Atvērt atkārtoti
-issues.reopen_comment_issue=Komentēt un atvērt atkārtoti
 issues.create_comment=Komentēt
 issues.closed_at=`slēdza šo problēmu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`atkārtoti atvēra šo problēmu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index adcbc6b66d..1b232fbf27 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -1211,9 +1211,7 @@ issues.context.quote_reply=Citeer antwoord
 issues.context.reference_issue=Verwijs in nieuw issue
 issues.context.edit=Bewerken
 issues.context.delete=Verwijder
-issues.close_comment_issue=Reageer en sluit
 issues.reopen_issue=Heropen
-issues.reopen_comment_issue=Heropen en geef commentaar
 issues.create_comment=Reageer
 issues.closed_at=`heeft dit probleem gesloten <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`heropende dit probleem <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 6fdec5183e..0807ae9478 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -1115,9 +1115,7 @@ issues.context.copy_link=Skopiuj link
 issues.context.quote_reply=Cytuj odpowiedź
 issues.context.edit=Edytuj
 issues.context.delete=Usuń
-issues.close_comment_issue=Skomentuj i zamknij
 issues.reopen_issue=Otwórz ponownie
-issues.reopen_comment_issue=Skomentuj i otwórz ponownie
 issues.create_comment=Skomentuj
 issues.closed_at=`zamknął(-ęła) to zgłoszenie <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`otworzył(-a) ponownie to zgłoszenie <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index 222abc1681..ec45e839c0 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -1461,9 +1461,7 @@ issues.no_content=Nenhuma descrição fornecida.
 issues.close=Fechar issue
 issues.comment_pull_merged_at=aplicou o merge do commit %[1]s em %[2]s %[3]s
 issues.comment_manually_pull_merged_at=aplicou o merge manual do commit %[1]s em %[2]s %[3]s
-issues.close_comment_issue=Comentar e fechar
 issues.reopen_issue=Reabrir
-issues.reopen_comment_issue=Comentar e reabrir
 issues.create_comment=Comentar
 issues.closed_at=`fechou esta issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`reabriu esta issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 4c05d7410e..f444cf6072 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1554,9 +1554,7 @@ issues.no_content=Nenhuma descrição fornecida.
 issues.close=Encerrar questão
 issues.comment_pull_merged_at=cometimento %[1]s integrado em %[2]s %[3]s
 issues.comment_manually_pull_merged_at=cometimento %[1]s integrado manualmente em %[2]s %[3]s
-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>`
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 33634105ff..464d602037 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -1440,9 +1440,7 @@ 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
-issues.close_comment_issue=Прокомментировать и закрыть
 issues.reopen_issue=Открыть снова
-issues.reopen_comment_issue=Прокомментировать и открыть снова
 issues.create_comment=Комментировать
 issues.closed_at=`закрыл(а) эту задачу <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`переоткрыл(а) эту проблему <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 16c11ef713..4d839b0977 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -1084,9 +1084,7 @@ issues.context.quote_reply=පිළිතුර උපුටා
 issues.context.reference_issue=නව නිකුතුවක යොමු කිරීම
 issues.context.edit=සංස්කරණය
 issues.context.delete=මකන්න
-issues.close_comment_issue=අදහස් දක්වා වසන්න
 issues.reopen_issue=නැවත විවෘත කරන්න
-issues.reopen_comment_issue=අදහස් දක්වා විවෘත කරන්න
 issues.create_comment=අදහස
 issues.closed_at=`මෙම ගැටළුව වසා <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`මෙම ගැටළුව නැවත විවෘත කරන ලදි <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index ee729911c3..56c7dc2fb3 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -929,9 +929,7 @@ issues.context.quote_reply=Citerat svar
 issues.context.reference_issue=Referens i nytt ärende
 issues.context.edit=Redigera
 issues.context.delete=Ta bort
-issues.close_comment_issue=Kommentera och stäng
 issues.reopen_issue=Återöppna
-issues.reopen_comment_issue=Kommentera och återöppna
 issues.create_comment=Kommentera
 issues.closed_at=`stängde ärendet <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`återöppnade detta ärende <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index f1ef7bd648..505f5743cd 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -1551,9 +1551,7 @@ issues.no_content=Herhangi bir açıklama sağlanmadı.
 issues.close=Konuyu Kapat
 issues.comment_pull_merged_at=%[1]s işlemesi, %[2]s dalına birleştirildi %[3]s
 issues.comment_manually_pull_merged_at=%[1]s işlemesi, %[2]s dalına elle birleştirildi %[3]s
-issues.close_comment_issue=Yorum Yap ve Kapat
 issues.reopen_issue=Yeniden aç
-issues.reopen_comment_issue=Yorum Yap ve Yeniden Aç
 issues.create_comment=Yorum yap
 issues.comment.blocked_user=Yorum oluşturulamıyor veya düzenlenemiyor, gönderen veya depo sahibi tarafından engellenmişsiniz.
 issues.closed_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> konusunu kapattı`
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index cc06c87d32..4b7bf86de8 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -1130,9 +1130,7 @@ issues.context.quote_reply=Цитувати відповідь
 issues.context.reference_issue=Посилання в новій задачі
 issues.context.edit=Редагувати
 issues.context.delete=Видалити
-issues.close_comment_issue=Прокоментувати і закрити
 issues.reopen_issue=Відкрити знову
-issues.reopen_comment_issue=Прокоментувати та відкрити знову
 issues.create_comment=Коментар
 issues.closed_at=`закрив цю задачу <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`повторно відкрив цю задачу <a id="%[1]s" href="#%[1]s">%[2]s</a>`
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 2d191521d6..7485d4d68f 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1378,6 +1378,7 @@ commitstatus.success=成功
 ext_issues=访问外部工单
 ext_issues.desc=链接到外部工单跟踪系统。
 
+projects.desc=在项目看板中管理工单和合并请求。
 projects.description=描述(可选)
 projects.description_placeholder=描述
 projects.create=创建项目
@@ -1441,6 +1442,7 @@ issues.new.clear_assignees=取消指派成员
 issues.new.no_assignees=未指派成员
 issues.new.no_reviewers=无审核者
 issues.new.blocked_user=无法创建工单,因为您已被仓库所有者屏蔽。
+issues.edit.already_changed=无法保存对工单的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改
 issues.edit.blocked_user=无法编辑内容,因为您已被仓库所有者或工单创建者屏蔽。
 issues.choose.get_started=开始
 issues.choose.open_external_link=开启
@@ -1552,9 +1554,7 @@ 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
-issues.close_comment_issue=评论并关闭
 issues.reopen_issue=重新开启
-issues.reopen_comment_issue=评论并重新开启
 issues.create_comment=评论
 issues.comment.blocked_user=无法创建或编辑评论,因为您已被仓库所有者或工单创建者屏蔽。
 issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此工单`
@@ -1756,6 +1756,7 @@ compare.compare_head=比较
 pulls.desc=启用合并请求和代码评审。
 pulls.new=创建合并请求
 pulls.new.blocked_user=无法创建合并请求,因为您已被仓库所有者屏蔽。
+pulls.edit.already_changed=无法保存对合并请求的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改
 pulls.view=查看拉取请求
 pulls.compare_changes=创建合并请求
 pulls.allow_edits_from_maintainers=允许维护者编辑
@@ -1901,6 +1902,7 @@ pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]
 
 pull.deleted_branch=(已删除): %s
 
+comments.edit.already_changed=无法保存对评论的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改
 
 milestones.new=新的里程碑
 milestones.closed=于 %s关闭
@@ -3637,6 +3639,7 @@ runs.pushed_by=推送者
 runs.invalid_workflow_helper=工作流配置文件无效。请检查您的配置文件: %s
 runs.no_matching_online_runner_helper=没有匹配标签的在线 runner: %s
 runs.no_job_without_needs=工作流必须包含至少一个没有依赖关系的作业。
+runs.no_job=工作流必须包含至少一个作业
 runs.actor=操作者
 runs.status=状态
 runs.actors_no_select=所有操作者
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index c3590b6acc..ae703233e8 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -1339,9 +1339,7 @@ issues.context.reference_issue=新增問題並參考
 issues.context.edit=編輯
 issues.context.delete=刪除
 issues.close=關閉問題
-issues.close_comment_issue=留言並關閉
 issues.reopen_issue=重新開放
-issues.reopen_comment_issue=留言並重新開放
 issues.create_comment=留言
 issues.closed_at=`關閉了這個問題 <a id="%[1]s" href="#%[1]s">%[2]s</a>`
 issues.reopened_at=`重新開放了這個問題 <a id="%[1]s" href="#%[1]s">%[2]s</a>`

From d612a24e3e8cd288047448df86b69d00484dd183 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 30 May 2024 10:24:22 +0800
Subject: [PATCH 068/131] Ignore FindRecentlyPushedNewBranches err (#31164)

Fix #31163
---
 routers/web/repo/view.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index e1498c0d58..386ef7be5c 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -1047,8 +1047,7 @@ func renderHomeCode(ctx *context.Context) {
 			baseRepoPerm.CanRead(unit_model.TypePullRequests) {
 			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
 			if err != nil {
-				ctx.ServerError("FindRecentlyPushedNewBranches", err)
-				return
+				log.Error("FindRecentlyPushedNewBranches failed: %v", err)
 			}
 		}
 	}

From 015efcd8bfd451ef593192eb43cfcfb7001f7861 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 30 May 2024 15:04:01 +0800
Subject: [PATCH 069/131] Use repo as of renderctx's member rather than a
 repoPath on metas (#29222)

Use a `gitrepo.Repository` in the markup's RenderContext but not store
the repository's path.
---
 models/issues/comment_code.go            |  3 +-
 models/repo/repo.go                      |  7 ++--
 modules/gitrepo/url.go                   |  8 ++++
 modules/markup/html.go                   | 11 +++---
 modules/markup/html_test.go              | 41 +++++++++++++-------
 modules/markup/main_test.go              | 14 +++++++
 modules/markup/markdown/main_test.go     | 21 ++++++++++
 modules/markup/markdown/markdown_test.go | 49 ++++++++++++++----------
 modules/markup/renderer.go               |  2 +
 routers/common/markup.go                 |  6 ++-
 routers/web/feed/convert.go              |  3 +-
 routers/web/repo/commit.go               |  1 +
 routers/web/repo/issue.go                |  5 +++
 routers/web/repo/milestone.go            |  2 +
 routers/web/repo/projects.go             |  2 +
 routers/web/repo/release.go              |  1 +
 routers/web/user/home.go                 |  1 +
 services/mailer/mail.go                  |  3 +-
 services/mailer/mail_release.go          |  3 +-
 19 files changed, 135 insertions(+), 48 deletions(-)
 create mode 100644 modules/gitrepo/url.go
 create mode 100644 modules/markup/main_test.go
 create mode 100644 modules/markup/markdown/main_test.go

diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index f860dacfac..6f23d3326a 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -113,7 +113,8 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 
 		var err error
 		if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-			Ctx: ctx,
+			Ctx:  ctx,
+			Repo: issue.Repo,
 			Links: markup.Links{
 				Base: issue.Repo.Link(),
 			},
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 5d5707d1ac..f02c55fc89 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -472,10 +472,9 @@ func (repo *Repository) MustOwner(ctx context.Context) *user_model.User {
 func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
 	if len(repo.RenderingMetas) == 0 {
 		metas := map[string]string{
-			"user":     repo.OwnerName,
-			"repo":     repo.Name,
-			"repoPath": repo.RepoPath(),
-			"mode":     "comment",
+			"user": repo.OwnerName,
+			"repo": repo.Name,
+			"mode": "comment",
 		}
 
 		unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
diff --git a/modules/gitrepo/url.go b/modules/gitrepo/url.go
new file mode 100644
index 0000000000..b355d0fa93
--- /dev/null
+++ b/modules/gitrepo/url.go
@@ -0,0 +1,8 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gitrepo
+
+func RepoGitURL(repo Repository) string {
+	return repoPath(repo)
+}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 2958dc9646..0af74d2680 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -16,7 +16,7 @@ import (
 
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/emoji"
-	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup/common"
 	"code.gitea.io/gitea/modules/references"
@@ -1140,7 +1140,7 @@ func emojiProcessor(ctx *RenderContext, node *html.Node) {
 // hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
 // are assumed to be in the same repository.
 func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
-	if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" {
+	if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || (ctx.Repo == nil && ctx.GitRepo == nil) {
 		return
 	}
 
@@ -1172,13 +1172,14 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
 		if !inCache {
 			if ctx.GitRepo == nil {
 				var err error
-				ctx.GitRepo, err = git.OpenRepository(ctx.Ctx, ctx.Metas["repoPath"])
+				var closer io.Closer
+				ctx.GitRepo, closer, err = gitrepo.RepositoryFromContextOrOpen(ctx.Ctx, ctx.Repo)
 				if err != nil {
-					log.Error("unable to open repository: %s Error: %v", ctx.Metas["repoPath"], err)
+					log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(ctx.Repo), err)
 					return
 				}
 				ctx.AddCancel(func() {
-					ctx.GitRepo.Close()
+					closer.Close()
 					ctx.GitRepo = nil
 				})
 			}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index a2ae18d777..0091397768 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -4,16 +4,13 @@
 package markup_test
 
 import (
-	"context"
 	"io"
-	"os"
 	"strings"
 	"testing"
 
-	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/emoji"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
@@ -22,18 +19,33 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-var localMetas = map[string]string{
-	"user":     "gogits",
-	"repo":     "gogs",
-	"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
+var (
+	testRepoOwnerName = "user13"
+	testRepoName      = "repo11"
+	localMetas        = map[string]string{
+		"user": testRepoOwnerName,
+		"repo": testRepoName,
+	}
+)
+
+type mockRepo struct {
+	OwnerName string
+	RepoName  string
 }
 
-func TestMain(m *testing.M) {
-	unittest.InitSettings()
-	if err := git.InitSimple(context.Background()); err != nil {
-		log.Fatal("git init failed, err: %v", err)
+func (m *mockRepo) GetOwnerName() string {
+	return m.OwnerName
+}
+
+func (m *mockRepo) GetName() string {
+	return m.RepoName
+}
+
+func newMockRepo(ownerName, repoName string) gitrepo.Repository {
+	return &mockRepo{
+		OwnerName: ownerName,
+		RepoName:  repoName,
 	}
-	os.Exit(m.Run())
 }
 
 func TestRender_Commits(t *testing.T) {
@@ -46,6 +58,7 @@ func TestRender_Commits(t *testing.T) {
 				AbsolutePrefix: true,
 				Base:           markup.TestRepoURL,
 			},
+			Repo:  newMockRepo(testRepoOwnerName, testRepoName),
 			Metas: localMetas,
 		}, input)
 		assert.NoError(t, err)
@@ -53,7 +66,7 @@ func TestRender_Commits(t *testing.T) {
 	}
 
 	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
-	repo := markup.TestRepoURL
+	repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/"
 	commit := util.URLJoin(repo, "commit", sha)
 	tree := util.URLJoin(repo, "tree", sha, "src")
 
diff --git a/modules/markup/main_test.go b/modules/markup/main_test.go
new file mode 100644
index 0000000000..a8f6f1c564
--- /dev/null
+++ b/modules/markup/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup_test
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m)
+}
diff --git a/modules/markup/markdown/main_test.go b/modules/markup/markdown/main_test.go
new file mode 100644
index 0000000000..f33eeb13b2
--- /dev/null
+++ b/modules/markup/markdown/main_test.go
@@ -0,0 +1,21 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+	"context"
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/markup"
+)
+
+func TestMain(m *testing.M) {
+	markup.Init(&markup.ProcessorHelper{
+		IsUsernameMentionable: func(ctx context.Context, username string) bool {
+			return username == "r-lyeh"
+		},
+	})
+	unittest.MainTest(m)
+}
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index bc6ad7fb3c..b4a7efa8dd 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -6,12 +6,11 @@ package markdown_test
 import (
 	"context"
 	"html/template"
-	"os"
 	"strings"
 	"testing"
 
-	"code.gitea.io/gitea/models/unittest"
 	"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"
@@ -25,28 +24,36 @@ import (
 )
 
 const (
-	AppURL  = "http://localhost:3000/"
-	FullURL = AppURL + "gogits/gogs/"
+	AppURL            = "http://localhost:3000/"
+	testRepoOwnerName = "user13"
+	testRepoName      = "repo11"
+	FullURL           = AppURL + testRepoOwnerName + "/" + testRepoName + "/"
 )
 
 // these values should match the const above
 var localMetas = map[string]string{
-	"user":     "gogits",
-	"repo":     "gogs",
-	"repoPath": "../../../tests/gitea-repositories-meta/user13/repo11.git/",
+	"user": testRepoOwnerName,
+	"repo": testRepoName,
 }
 
-func TestMain(m *testing.M) {
-	unittest.InitSettings()
-	if err := git.InitSimple(context.Background()); err != nil {
-		log.Fatal("git init failed, err: %v", err)
+type mockRepo struct {
+	OwnerName string
+	RepoName  string
+}
+
+func (m *mockRepo) GetOwnerName() string {
+	return m.OwnerName
+}
+
+func (m *mockRepo) GetName() string {
+	return m.RepoName
+}
+
+func newMockRepo(ownerName, repoName string) gitrepo.Repository {
+	return &mockRepo{
+		OwnerName: ownerName,
+		RepoName:  repoName,
 	}
-	markup.Init(&markup.ProcessorHelper{
-		IsUsernameMentionable: func(ctx context.Context, username string) bool {
-			return username == "r-lyeh"
-		},
-	})
-	os.Exit(m.Run())
 }
 
 func TestRender_StandardLinks(t *testing.T) {
@@ -133,11 +140,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="/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
+<p>See commit <a href="/` + testRepoOwnerName + `/` + testRepoName + `/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
 <p>Ideas and codes</p>
 <ul>
 <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>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="` + FullURL + `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>
@@ -222,7 +229,7 @@ See commit 65f1bf27bc
 Ideas and codes
 
 - Bezier widget (by @r-lyeh) ` + AppURL + `ocornut/imgui/issues/786
-- Bezier widget (by @r-lyeh) ` + AppURL + `gogits/gogs/issues/786
+- Bezier widget (by @r-lyeh) ` + FullURL + `issues/786
 - Node graph editors https://github.com/ocornut/imgui/issues/306
 - [[Memory Editor|memory_editor_example]]
 - [[Plot var helper|plot_var_example]]`,
@@ -299,6 +306,7 @@ func TestTotal_RenderWiki(t *testing.T) {
 			Links: markup.Links{
 				Base: FullURL,
 			},
+			Repo:   newMockRepo(testRepoOwnerName, testRepoName),
 			Metas:  localMetas,
 			IsWiki: true,
 		}, sameCases[i])
@@ -344,6 +352,7 @@ func TestTotal_RenderString(t *testing.T) {
 				Base:       FullURL,
 				BranchPath: "master",
 			},
+			Repo:  newMockRepo(testRepoOwnerName, testRepoName),
 			Metas: localMetas,
 		}, sameCases[i])
 		assert.NoError(t, err)
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 005fcc278b..f836f12ad3 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -16,6 +16,7 @@ import (
 	"sync"
 
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
@@ -77,6 +78,7 @@ type RenderContext struct {
 	Metas            map[string]string
 	DefaultLink      string
 	GitRepo          *git.Repository
+	Repo             gitrepo.Repository
 	ShaExistCache    map[string]bool
 	cancelFn         func()
 	SidebarTocNode   ast.Node
diff --git a/routers/common/markup.go b/routers/common/markup.go
index 2d5638ef61..f7d096008a 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"strings"
 
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
@@ -66,7 +67,9 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 	}
 
 	meta := map[string]string{}
+	var repoCtx *repo_model.Repository
 	if repo != nil && repo.Repository != nil {
+		repoCtx = repo.Repository
 		if mode == "comment" {
 			meta = repo.Repository.ComposeMetas(ctx)
 		} else {
@@ -78,7 +81,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 	}
 
 	if err := markup.Render(&markup.RenderContext{
-		Ctx: ctx,
+		Ctx:  ctx,
+		Repo: repoCtx,
 		Links: markup.Links{
 			AbsolutePrefix: true,
 			Base:           urlPrefix,
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 20fcda6664..cb62858631 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -297,7 +297,8 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (
 
 		link := &feeds.Link{Href: rel.HTMLURL()}
 		content, err = markdown.RenderString(&markup.RenderContext{
-			Ctx: ctx,
+			Ctx:  ctx,
+			Repo: rel.Repo,
 			Links: markup.Links{
 				Base: rel.Repo.Link(),
 			},
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index a2c6ac33e8..7b5e72593f 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -382,6 +382,7 @@ func Diff(ctx *context.Context) {
 			},
 			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 			GitRepo: ctx.Repo.GitRepo,
+			Repo:    ctx.Repo.Repository,
 			Ctx:     ctx,
 		}, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
 		if err != nil {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index ce459f23b9..18f975b4a6 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1466,6 +1466,7 @@ func ViewIssue(ctx *context.Context) {
 		},
 		Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 		GitRepo: ctx.Repo.GitRepo,
+		Repo:    ctx.Repo.Repository,
 		Ctx:     ctx,
 	}, issue.Content)
 	if err != nil {
@@ -1622,6 +1623,7 @@ func ViewIssue(ctx *context.Context) {
 				},
 				Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 				GitRepo: ctx.Repo.GitRepo,
+				Repo:    ctx.Repo.Repository,
 				Ctx:     ctx,
 			}, comment.Content)
 			if err != nil {
@@ -1699,6 +1701,7 @@ func ViewIssue(ctx *context.Context) {
 				},
 				Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 				GitRepo: ctx.Repo.GitRepo,
+				Repo:    ctx.Repo.Repository,
 				Ctx:     ctx,
 			}, comment.Content)
 			if err != nil {
@@ -2276,6 +2279,7 @@ func UpdateIssueContent(ctx *context.Context) {
 		},
 		Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 		GitRepo: ctx.Repo.GitRepo,
+		Repo:    ctx.Repo.Repository,
 		Ctx:     ctx,
 	}, issue.Content)
 	if err != nil {
@@ -3196,6 +3200,7 @@ func UpdateCommentContent(ctx *context.Context) {
 			},
 			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 			GitRepo: ctx.Repo.GitRepo,
+			Repo:    ctx.Repo.Repository,
 			Ctx:     ctx,
 		}, comment.Content)
 		if err != nil {
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 95a4fe60cc..c6c8cb5cfb 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -86,6 +86,7 @@ func Milestones(ctx *context.Context) {
 			},
 			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 			GitRepo: ctx.Repo.GitRepo,
+			Repo:    ctx.Repo.Repository,
 			Ctx:     ctx,
 		}, m.Content)
 		if err != nil {
@@ -282,6 +283,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
 		},
 		Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 		GitRepo: ctx.Repo.GitRepo,
+		Repo:    ctx.Repo.Repository,
 		Ctx:     ctx,
 	}, milestone.Content)
 	if err != nil {
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 9ce5535a0e..2e32f478aa 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -96,6 +96,7 @@ func Projects(ctx *context.Context) {
 			},
 			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 			GitRepo: ctx.Repo.GitRepo,
+			Repo:    ctx.Repo.Repository,
 			Ctx:     ctx,
 		}, projects[i].Description)
 		if err != nil {
@@ -357,6 +358,7 @@ func ViewProject(ctx *context.Context) {
 		},
 		Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 		GitRepo: ctx.Repo.GitRepo,
+		Repo:    ctx.Repo.Repository,
 		Ctx:     ctx,
 	}, project.Description)
 	if err != nil {
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 7ba23f0701..8ba2adf3f1 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -119,6 +119,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
 			},
 			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
 			GitRepo: ctx.Repo.GitRepo,
+			Repo:    ctx.Repo.Repository,
 			Ctx:     ctx,
 		}, r.Note)
 		if err != nil {
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index c3f34039e9..b03a514030 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -262,6 +262,7 @@ func Milestones(ctx *context.Context) {
 			},
 			Metas: milestones[i].Repo.ComposeMetas(ctx),
 			Ctx:   ctx,
+			Repo:  milestones[i].Repo,
 		}, milestones[i].Content)
 		if err != nil {
 			ctx.ServerError("RenderString", err)
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 04194dcf26..000cc835c8 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -220,7 +220,8 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 
 	// This is the body of the new issue or comment, not the mail body
 	body, err := markdown.RenderString(&markup.RenderContext{
-		Ctx: ctx,
+		Ctx:  ctx,
+		Repo: ctx.Issue.Repo,
 		Links: markup.Links{
 			AbsolutePrefix: true,
 			Base:           ctx.Issue.Repo.HTMLURL(),
diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go
index 2aac21e552..b7a4da0db9 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -57,7 +57,8 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
 
 	var err error
 	rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
-		Ctx: ctx,
+		Ctx:  ctx,
+		Repo: rel.Repo,
 		Links: markup.Links{
 			Base: rel.Repo.HTMLURL(),
 		},

From fb7b743bd0f305a6462896398bcba2a74c6e391e Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 30 May 2024 15:33:50 +0800
Subject: [PATCH 070/131] Azure blob storage support (#30995)

This PR implemented object storages(LFS/Packages/Attachments and etc.)
for Azure Blob Storage. It depends on azure official golang SDK and can
support both the azure blob storage cloud service and azurite mock
server.

Replace #25458
Fix #22527

- [x] CI Tests
- [x] integration test, MSSQL integration tests will now based on
azureblob
  - [x] unit test
- [x] CLI Migrate Storage
- [x] Documentation for configuration added

------

TODO (other PRs):
- [ ] Improve performance of `blob download`.

---------

Co-authored-by: yp05327 <576951401@qq.com>
---
 .devcontainer/devcontainer.json               |   3 +-
 .github/workflows/pull-db-tests.yml           |  12 +-
 assets/go-licenses.json                       |  15 +
 cmd/migrate_storage.go                        |  41 ++-
 custom/conf/app.example.ini                   |  49 ++-
 .../config-cheat-sheet.en-us.md               |   8 +-
 .../config-cheat-sheet.zh-cn.md               |   7 +-
 go.mod                                        |   3 +
 go.sum                                        |  14 +-
 models/repo/attachment.go                     |   8 +-
 modules/packages/content_store.go             |   2 +-
 modules/setting/storage.go                    |  90 ++++-
 modules/setting/storage_test.go               | 112 ++++++
 modules/storage/azureblob.go                  | 322 ++++++++++++++++++
 modules/storage/azureblob_test.go             |  56 +++
 modules/storage/minio_test.go                 |   2 +-
 modules/storage/storage_test.go               |   1 +
 modules/util/io.go                            |  21 ++
 routers/api/actions/artifacts.go              |   2 +-
 routers/api/actions/artifacts_chunks.go       |   2 +-
 routers/api/actions/artifactsv4.go            |   2 +-
 routers/api/v1/repo/file.go                   |   4 +-
 routers/web/base.go                           |   2 +-
 routers/web/repo/actions/view.go              |   2 +-
 routers/web/repo/attachment.go                |   2 +-
 routers/web/repo/download.go                  |   4 +-
 routers/web/repo/repo.go                      |   2 +-
 services/lfs/server.go                        |   2 +-
 .../integration/api_packages_generic_test.go  |  29 +-
 tests/mssql.ini.tmpl                          |  12 +-
 tests/pgsql.ini.tmpl                          |   3 -
 31 files changed, 779 insertions(+), 55 deletions(-)
 create mode 100644 modules/storage/azureblob.go
 create mode 100644 modules/storage/azureblob_test.go

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index d391cf78cf..c32c5da82c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -26,7 +26,8 @@
         "ms-azuretools.vscode-docker",
         "vitest.explorer",
         "qwtel.sqlite-viewer",
-        "GitHub.vscode-pull-request-github"
+        "GitHub.vscode-pull-request-github",
+        "Azurite.azurite"
       ]
     }
   },
diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index 61c0391509..246884f24b 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -119,6 +119,10 @@ jobs:
           MINIO_SECRET_KEY: 12345678
         ports:
           - "9000:9000"
+      devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
+        image: mcr.microsoft.com/azure-storage/azurite:latest
+        ports:
+          - 10000:10000
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-go@v5
@@ -126,7 +130,7 @@ jobs:
           go-version-file: go.mod
           check-latest: true
       - name: Add hosts to /etc/hosts
-        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
+        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
       - run: make deps-backend
       - run: make backend
         env:
@@ -204,6 +208,10 @@ jobs:
           SA_PASSWORD: MwantsaSecurePassword1
         ports:
           - "1433:1433"
+      devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
+        image: mcr.microsoft.com/azure-storage/azurite:latest
+        ports:
+          - 10000:10000
     steps:
       - uses: actions/checkout@v4
       - uses: actions/setup-go@v5
@@ -211,7 +219,7 @@ jobs:
           go-version-file: go.mod
           check-latest: true
       - name: Add hosts to /etc/hosts
-        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql" | sudo tee -a /etc/hosts'
+        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
       - run: make deps-backend
       - run: make backend
         env:
diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index b8905da284..c013b4c482 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -79,6 +79,21 @@
     "path": "github.com/42wim/sshsig/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/Azure/azure-sdk-for-go/sdk/azcore",
+    "path": "github.com/Azure/azure-sdk-for-go/sdk/azcore/LICENSE.txt",
+    "licenseText": "MIT License\n\nCopyright (c) Microsoft Corporation.\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"
+  },
+  {
+    "name": "github.com/Azure/azure-sdk-for-go/sdk/internal",
+    "path": "github.com/Azure/azure-sdk-for-go/sdk/internal/LICENSE.txt",
+    "licenseText": "MIT License\n\nCopyright (c) Microsoft Corporation.\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"
+  },
+  {
+    "name": "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob",
+    "path": "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/LICENSE.txt",
+    "licenseText": "    MIT License\n\n    Copyright (c) Microsoft Corporation. All rights reserved.\n\n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE"
+  },
   {
     "name": "github.com/Azure/go-ntlmssp",
     "path": "github.com/Azure/go-ntlmssp/LICENSE",
diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go
index 7d1ef052ff..1720b6fb53 100644
--- a/cmd/migrate_storage.go
+++ b/cmd/migrate_storage.go
@@ -40,7 +40,7 @@ var CmdMigrateStorage = &cli.Command{
 			Name:    "storage",
 			Aliases: []string{"s"},
 			Value:   "",
-			Usage:   "New storage type: local (default) or minio",
+			Usage:   "New storage type: local (default), minio or azureblob",
 		},
 		&cli.StringFlag{
 			Name:    "path",
@@ -48,6 +48,7 @@ var CmdMigrateStorage = &cli.Command{
 			Value:   "",
 			Usage:   "New storage placement if store is local (leave blank for default)",
 		},
+		// Minio Storage special configurations
 		&cli.StringFlag{
 			Name:  "minio-endpoint",
 			Value: "",
@@ -96,6 +97,32 @@ var CmdMigrateStorage = &cli.Command{
 			Value: "",
 			Usage: "Minio bucket lookup type",
 		},
+		// Azure Blob Storage special configurations
+		&cli.StringFlag{
+			Name:  "azureblob-endpoint",
+			Value: "",
+			Usage: "Azure Blob storage endpoint",
+		},
+		&cli.StringFlag{
+			Name:  "azureblob-account-name",
+			Value: "",
+			Usage: "Azure Blob storage account name",
+		},
+		&cli.StringFlag{
+			Name:  "azureblob-account-key",
+			Value: "",
+			Usage: "Azure Blob storage account key",
+		},
+		&cli.StringFlag{
+			Name:  "azureblob-container",
+			Value: "",
+			Usage: "Azure Blob storage container",
+		},
+		&cli.StringFlag{
+			Name:  "azureblob-base-path",
+			Value: "",
+			Usage: "Azure Blob storage base path",
+		},
 	},
 }
 
@@ -228,6 +255,18 @@ func runMigrateStorage(ctx *cli.Context) error {
 					BucketLookUpType:   ctx.String("minio-bucket-lookup-type"),
 				},
 			})
+	case string(setting.AzureBlobStorageType):
+		dstStorage, err = storage.NewAzureBlobStorage(
+			stdCtx,
+			&setting.Storage{
+				AzureBlobConfig: setting.AzureBlobStorageConfig{
+					Endpoint:    ctx.String("azureblob-endpoint"),
+					AccountName: ctx.String("azureblob-account-name"),
+					AccountKey:  ctx.String("azureblob-account-key"),
+					Container:   ctx.String("azureblob-container"),
+					BasePath:    ctx.String("azureblob-base-path"),
+				},
+			})
 	default:
 		return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))
 	}
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 7c05e7fefd..be5d632f54 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1862,7 +1862,7 @@ LEVEL = Info
 ;STORAGE_TYPE = local
 ;;
 ;; Allows the storage driver to redirect to authenticated URLs to serve files directly
-;; Currently, only `minio` is supported.
+;; Currently, only `minio` and `azureblob` is supported.
 ;SERVE_DIRECT = false
 ;;
 ;; Path for attachments. Defaults to `attachments`. Only available when STORAGE_TYPE is `local`
@@ -1901,6 +1901,21 @@ LEVEL = Info
 ;;
 ;; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
 ;MINIO_BUCKET_LOOKUP_TYPE = auto
+;; Azure Blob endpoint to connect only available when STORAGE_TYPE is `azureblob`,
+;; e.g. https://accountname.blob.core.windows.net or http://127.0.0.1:10000/devstoreaccount1
+;AZURE_BLOB_ENDPOINT =
+;;
+;; Azure Blob account name to connect only available when STORAGE_TYPE is `azureblob`
+;AZURE_BLOB_ACCOUNT_NAME =
+;;
+;; Azure Blob account key to connect only available when STORAGE_TYPE is `azureblob`
+;AZURE_BLOB_ACCOUNT_KEY =
+;;
+;; Azure Blob container to store the attachments only available when STORAGE_TYPE is `azureblob`
+;AZURE_BLOB_CONTAINER = gitea
+;;
+;; override the azure blob base path if storage type is azureblob
+;AZURE_BLOB_BASE_PATH = attachments/
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2460,6 +2475,11 @@ LEVEL = Info
 ;STORAGE_TYPE = local
 ;; override the minio base path if storage type is minio
 ;MINIO_BASE_PATH = packages/
+;; override the azure blob base path if storage type is azureblob
+;AZURE_BLOB_BASE_PATH = packages/
+;; Allows the storage driver to redirect to authenticated URLs to serve files directly
+;; Currently, only `minio` and `azureblob` is supported.
+;SERVE_DIRECT = false
 ;;
 ;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload`
 ;CHUNKED_UPLOAD_PATH = tmp/package-upload
@@ -2533,6 +2553,8 @@ LEVEL = Info
 ;;
 ;; override the minio base path if storage type is minio
 ;MINIO_BASE_PATH = repo-archive/
+;; override the azure blob base path if storage type is azureblob
+;AZURE_BLOB_BASE_PATH = repo-archive/
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2554,8 +2576,15 @@ LEVEL = Info
 ;; Where your lfs files reside, default is data/lfs.
 ;PATH = data/lfs
 ;;
+;; Allows the storage driver to redirect to authenticated URLs to serve files directly
+;; Currently, only `minio` and `azureblob` is supported.
+;SERVE_DIRECT = false
+;;
 ;; override the minio base path if storage type is minio
 ;MINIO_BASE_PATH = lfs/
+;;
+;; override the azure blob base path if storage type is azureblob
+;AZURE_BLOB_BASE_PATH = lfs/
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2570,7 +2599,7 @@ LEVEL = Info
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;; customize storage
-;[storage.my_minio]
+;[storage.minio]
 ;STORAGE_TYPE = minio
 ;;
 ;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
@@ -2600,6 +2629,22 @@ LEVEL = Info
 ;; Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
 ;MINIO_BUCKET_LOOKUP_TYPE = auto
 
+;[storage.azureblob]
+;STORAGE_TYPE = azureblob
+;;
+;; Azure Blob endpoint to connect only available when STORAGE_TYPE is `azureblob`,
+;; e.g. https://accountname.blob.core.windows.net or http://127.0.0.1:10000/devstoreaccount1
+;AZURE_BLOB_ENDPOINT =
+;;
+;; Azure Blob account name to connect only available when STORAGE_TYPE is `azureblob`
+;AZURE_BLOB_ACCOUNT_NAME =
+;;
+;; Azure Blob account key to connect only available when STORAGE_TYPE is `azureblob`
+;AZURE_BLOB_ACCOUNT_KEY =
+;;
+;; Azure Blob container to store the attachments only available when STORAGE_TYPE is `azureblob`
+;AZURE_BLOB_CONTAINER = gitea
+
 ;[proxy]
 ;; Enable the proxy, all requests to external via HTTP will be affected
 ;PROXY_ENABLED = false
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index 2c15d161ea..aabf1b20d8 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -1287,7 +1287,7 @@ is `data/lfs` and the default of `MINIO_BASE_PATH` is `lfs/`.
 
 Default storage configuration for attachments, lfs, avatars, repo-avatars, repo-archive, packages, actions_log, actions_artifact.
 
-- `STORAGE_TYPE`: **local**: Storage type, `local` for local disk or `minio` for s3 compatible object storage service.
+- `STORAGE_TYPE`: **local**: Storage type, `local` for local disk, `minio` for s3 compatible object storage service, `azureblob` for azure blob storage service.
 - `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
 - `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
 - `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`. If not provided and STORAGE_TYPE is `minio`, will search for credentials in known environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
@@ -1298,6 +1298,12 @@ Default storage configuration for attachments, lfs, avatars, repo-avatars, repo-
 - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
 - `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio bucket lookup method defaults to auto mode; set it to `dns` for virtual host style or `path` for path style, only available when STORAGE_TYPE is `minio`
 
+- `AZURE_BLOB_ENDPOINT`: **_empty_**: Azure Blob endpoint to connect only available when STORAGE_TYPE is `azureblob`,
+ e.g. https://accountname.blob.core.windows.net or http://127.0.0.1:10000/devstoreaccount1
+- `AZURE_BLOB_ACCOUNT_NAME`: **_empty_**: Azure Blob account name to connect only available when STORAGE_TYPE is `azureblob`
+- `AZURE_BLOB_ACCOUNT_KEY`: **_empty_**: Azure Blob account key to connect only available when STORAGE_TYPE is `azureblob`
+- `AZURE_BLOB_CONTAINER`: **gitea**: Azure Blob container to store the data only available when STORAGE_TYPE is `azureblob`
+
 The recommended storage configuration for minio like below:
 
 ```ini
diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md
index 3c6ac8c00a..7d51c758b6 100644
--- a/docs/content/administration/config-cheat-sheet.zh-cn.md
+++ b/docs/content/administration/config-cheat-sheet.zh-cn.md
@@ -1208,7 +1208,7 @@ ALLOW_DATA_URI_IMAGES = true
 
 默认的附件、lfs、头像、仓库头像、仓库归档、软件包、操作日志、操作艺术品的存储配置。
 
-- `STORAGE_TYPE`:**local**:存储类型,`local` 表示本地磁盘,`minio` 表示 S3 兼容的对象存储服务。
+- `STORAGE_TYPE`:**local**:存储类型,`local` 表示本地磁盘,`minio` 表示 S3,`azureblob` 表示 azure 对象存储。
 - `SERVE_DIRECT`:**false**:允许存储驱动程序重定向到经过身份验证的 URL 以直接提供文件。目前,仅支持通过签名的 URL 提供 Minio/S3,本地不执行任何操作。
 - `MINIO_ENDPOINT`:**localhost:9000**:连接的 Minio 终端点,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
 - `MINIO_ACCESS_KEY_ID`:Minio 的 accessKeyID,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
@@ -1219,6 +1219,11 @@ ALLOW_DATA_URI_IMAGES = true
 - `MINIO_INSECURE_SKIP_VERIFY`:**false**:Minio 跳过 SSL 验证,仅在 `STORAGE_TYPE` 为 `minio` 时可用。
 - `MINIO_BUCKET_LOOKUP_TYPE`: **auto**: Minio的bucket查找方式默认为`auto`模式,可将其设置为`dns`(虚拟托管样式)或`path`(路径样式),仅当`STORAGE_TYPE`为`minio`时可用。
 
+- `AZURE_BLOB_ENDPOINT`: **_empty_**: Azure Blob 终端点,仅在 `STORAGE_TYPE` 为 `azureblob` 时可用。例如:https://accountname.blob.core.windows.net 或 http://127.0.0.1:10000/devstoreaccount1
+- `AZURE_BLOB_ACCOUNT_NAME`: **_empty_**: Azure Blob 账号名,仅在 `STORAGE_TYPE` 为 `azureblob` 时可用。
+- `AZURE_BLOB_ACCOUNT_KEY`: **_empty_**: Azure Blob 访问密钥,仅在 `STORAGE_TYPE` 为 `azureblob` 时可用。
+- `AZURE_BLOB_CONTAINER`: **gitea**: 用于存储数据的 Azure Blob 容器名,仅在 `STORAGE_TYPE` 为 `azureblob` 时可用。
+
 建议的 minio 存储配置如下:
 
 ```ini
diff --git a/go.mod b/go.mod
index 8afefc6367..87f2b00e6a 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,8 @@ require (
 	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/azure-sdk-for-go/sdk/azcore v1.11.1
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
 	github.com/ProtonMail/go-crypto v1.0.0
 	github.com/PuerkitoBio/goquery v1.9.1
@@ -130,6 +132,7 @@ require (
 	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/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // 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
diff --git a/go.sum b/go.sum
index 1d493f4ca4..84f7121908 100644
--- a/go.sum
+++ b/go.sum
@@ -38,16 +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 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/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
 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/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
 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/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E=
 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=
@@ -227,6 +231,8 @@ 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 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
+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=
diff --git a/models/repo/attachment.go b/models/repo/attachment.go
index 9b0de11fdc..fa4f6c47e6 100644
--- a/models/repo/attachment.go
+++ b/models/repo/attachment.go
@@ -5,11 +5,14 @@ package repo
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/url"
+	"os"
 	"path"
 
 	"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/timeutil"
@@ -188,7 +191,10 @@ func DeleteAttachments(ctx context.Context, attachments []*Attachment, remove bo
 	if remove {
 		for i, a := range attachments {
 			if err := storage.Attachments.Delete(a.RelativePath()); err != nil {
-				return i, err
+				if !errors.Is(err, os.ErrNotExist) {
+					return i, err
+				}
+				log.Warn("Attachment file not found when deleting: %s", a.RelativePath())
 			}
 		}
 	}
diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go
index da93e6cf6b..2108be64d2 100644
--- a/modules/packages/content_store.go
+++ b/modules/packages/content_store.go
@@ -34,7 +34,7 @@ func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) {
 }
 
 func (s *ContentStore) ShouldServeDirect() bool {
-	return setting.Packages.Storage.MinioConfig.ServeDirect
+	return setting.Packages.Storage.ServeDirect()
 }
 
 func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string) (*url.URL, error) {
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index d80a61a45e..d44c968423 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -18,11 +18,14 @@ const (
 	LocalStorageType StorageType = "local"
 	// MinioStorageType is the type descriptor for minio storage
 	MinioStorageType StorageType = "minio"
+	// AzureBlobStorageType is the type descriptor for azure blob storage
+	AzureBlobStorageType StorageType = "azureblob"
 )
 
 var storageTypes = []StorageType{
 	LocalStorageType,
 	MinioStorageType,
+	AzureBlobStorageType,
 }
 
 // IsValidStorageType returns true if the given storage type is valid
@@ -50,25 +53,55 @@ type MinioStorageConfig struct {
 	BucketLookUpType   string `ini:"MINIO_BUCKET_LOOKUP_TYPE" json:",omitempty"`
 }
 
+func (cfg *MinioStorageConfig) ToShadow() {
+	if cfg.AccessKeyID != "" {
+		cfg.AccessKeyID = "******"
+	}
+	if cfg.SecretAccessKey != "" {
+		cfg.SecretAccessKey = "******"
+	}
+}
+
+// MinioStorageConfig represents the configuration for a minio storage
+type AzureBlobStorageConfig struct {
+	Endpoint    string `ini:"AZURE_BLOB_ENDPOINT" json:",omitempty"`
+	AccountName string `ini:"AZURE_BLOB_ACCOUNT_NAME" json:",omitempty"`
+	AccountKey  string `ini:"AZURE_BLOB_ACCOUNT_KEY" json:",omitempty"`
+	Container   string `ini:"AZURE_BLOB_CONTAINER" json:",omitempty"`
+	BasePath    string `ini:"AZURE_BLOB_BASE_PATH" json:",omitempty"`
+	ServeDirect bool   `ini:"SERVE_DIRECT"`
+}
+
+func (cfg *AzureBlobStorageConfig) ToShadow() {
+	if cfg.AccountKey != "" {
+		cfg.AccountKey = "******"
+	}
+	if cfg.AccountName != "" {
+		cfg.AccountName = "******"
+	}
+}
+
 // Storage represents configuration of storages
 type Storage struct {
-	Type          StorageType        // local or minio
-	Path          string             `json:",omitempty"` // for local type
-	TemporaryPath string             `json:",omitempty"`
-	MinioConfig   MinioStorageConfig // for minio type
+	Type            StorageType            // local or minio or azureblob
+	Path            string                 `json:",omitempty"` // for local type
+	TemporaryPath   string                 `json:",omitempty"`
+	MinioConfig     MinioStorageConfig     // for minio type
+	AzureBlobConfig AzureBlobStorageConfig // for azureblob type
 }
 
 func (storage *Storage) ToShadowCopy() Storage {
 	shadowStorage := *storage
-	if shadowStorage.MinioConfig.AccessKeyID != "" {
-		shadowStorage.MinioConfig.AccessKeyID = "******"
-	}
-	if shadowStorage.MinioConfig.SecretAccessKey != "" {
-		shadowStorage.MinioConfig.SecretAccessKey = "******"
-	}
+	shadowStorage.MinioConfig.ToShadow()
+	shadowStorage.AzureBlobConfig.ToShadow()
 	return shadowStorage
 }
 
+func (storage *Storage) ServeDirect() bool {
+	return (storage.Type == MinioStorageType && storage.MinioConfig.ServeDirect) ||
+		(storage.Type == AzureBlobStorageType && storage.AzureBlobConfig.ServeDirect)
+}
+
 const storageSectionName = "storage"
 
 func getDefaultStorageSection(rootCfg ConfigProvider) ConfigSection {
@@ -84,6 +117,10 @@ func getDefaultStorageSection(rootCfg ConfigProvider) ConfigSection {
 	storageSec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false)
 	storageSec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default")
 	storageSec.Key("MINIO_BUCKET_LOOKUP_TYPE").MustString("auto")
+	storageSec.Key("AZURE_BLOB_ENDPOINT").MustString("")
+	storageSec.Key("AZURE_BLOB_ACCOUNT_NAME").MustString("")
+	storageSec.Key("AZURE_BLOB_ACCOUNT_KEY").MustString("")
+	storageSec.Key("AZURE_BLOB_CONTAINER").MustString("gitea")
 	return storageSec
 }
 
@@ -107,6 +144,8 @@ func getStorage(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (*S
 		return getStorageForLocal(targetSec, overrideSec, tp, name)
 	case string(MinioStorageType):
 		return getStorageForMinio(targetSec, overrideSec, tp, name)
+	case string(AzureBlobStorageType):
+		return getStorageForAzureBlob(targetSec, overrideSec, tp, name)
 	default:
 		return nil, fmt.Errorf("unsupported storage type %q", targetType)
 	}
@@ -247,7 +286,7 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
 	return &storage, nil
 }
 
-func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) {
+func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl
 	var storage Storage
 	storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String())
 	if err := targetSec.MapTo(&storage.MinioConfig); err != nil {
@@ -275,3 +314,32 @@ func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType,
 	}
 	return &storage, nil
 }
+
+func getStorageForAzureBlob(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl
+	var storage Storage
+	storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String())
+	if err := targetSec.MapTo(&storage.AzureBlobConfig); err != nil {
+		return nil, fmt.Errorf("map azure blob config failed: %v", err)
+	}
+
+	var defaultPath string
+	if storage.AzureBlobConfig.BasePath != "" {
+		if tp == targetSecIsStorage || tp == targetSecIsDefault {
+			defaultPath = strings.TrimSuffix(storage.AzureBlobConfig.BasePath, "/") + "/" + name + "/"
+		} else {
+			defaultPath = storage.AzureBlobConfig.BasePath
+		}
+	}
+	if defaultPath == "" {
+		defaultPath = name + "/"
+	}
+
+	if overrideSec != nil {
+		storage.AzureBlobConfig.ServeDirect = ConfigSectionKeyBool(overrideSec, "SERVE_DIRECT", storage.AzureBlobConfig.ServeDirect)
+		storage.AzureBlobConfig.BasePath = ConfigSectionKeyString(overrideSec, "AZURE_BLOB_BASE_PATH", defaultPath)
+		storage.AzureBlobConfig.Container = ConfigSectionKeyString(overrideSec, "AZURE_BLOB_CONTAINER", storage.AzureBlobConfig.Container)
+	} else {
+		storage.AzureBlobConfig.BasePath = defaultPath
+	}
+	return &storage, nil
+}
diff --git a/modules/setting/storage_test.go b/modules/setting/storage_test.go
index 6f38bf1d55..44a5de6826 100644
--- a/modules/setting/storage_test.go
+++ b/modules/setting/storage_test.go
@@ -97,6 +97,44 @@ STORAGE_TYPE = minio
 	assert.EqualValues(t, "repo-avatars/", RepoAvatar.Storage.MinioConfig.BasePath)
 }
 
+func Test_getStorageInheritStorageTypeAzureBlob(t *testing.T) {
+	iniStr := `
+[storage]
+STORAGE_TYPE = azureblob
+`
+	cfg, err := NewConfigProviderFromData(iniStr)
+	assert.NoError(t, err)
+
+	assert.NoError(t, loadPackagesFrom(cfg))
+	assert.EqualValues(t, "azureblob", Packages.Storage.Type)
+	assert.EqualValues(t, "gitea", Packages.Storage.AzureBlobConfig.Container)
+	assert.EqualValues(t, "packages/", Packages.Storage.AzureBlobConfig.BasePath)
+
+	assert.NoError(t, loadRepoArchiveFrom(cfg))
+	assert.EqualValues(t, "azureblob", RepoArchive.Storage.Type)
+	assert.EqualValues(t, "gitea", RepoArchive.Storage.AzureBlobConfig.Container)
+	assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.AzureBlobConfig.BasePath)
+
+	assert.NoError(t, loadActionsFrom(cfg))
+	assert.EqualValues(t, "azureblob", Actions.LogStorage.Type)
+	assert.EqualValues(t, "gitea", Actions.LogStorage.AzureBlobConfig.Container)
+	assert.EqualValues(t, "actions_log/", Actions.LogStorage.AzureBlobConfig.BasePath)
+
+	assert.EqualValues(t, "azureblob", Actions.ArtifactStorage.Type)
+	assert.EqualValues(t, "gitea", Actions.ArtifactStorage.AzureBlobConfig.Container)
+	assert.EqualValues(t, "actions_artifacts/", Actions.ArtifactStorage.AzureBlobConfig.BasePath)
+
+	assert.NoError(t, loadAvatarsFrom(cfg))
+	assert.EqualValues(t, "azureblob", Avatar.Storage.Type)
+	assert.EqualValues(t, "gitea", Avatar.Storage.AzureBlobConfig.Container)
+	assert.EqualValues(t, "avatars/", Avatar.Storage.AzureBlobConfig.BasePath)
+
+	assert.NoError(t, loadRepoAvatarFrom(cfg))
+	assert.EqualValues(t, "azureblob", RepoAvatar.Storage.Type)
+	assert.EqualValues(t, "gitea", RepoAvatar.Storage.AzureBlobConfig.Container)
+	assert.EqualValues(t, "repo-avatars/", RepoAvatar.Storage.AzureBlobConfig.BasePath)
+}
+
 type testLocalStoragePathCase struct {
 	loader       func(rootCfg ConfigProvider) error
 	storagePtr   **Storage
@@ -465,3 +503,77 @@ MINIO_BASE_PATH = /lfs
 	assert.EqualValues(t, true, LFS.Storage.MinioConfig.UseSSL)
 	assert.EqualValues(t, "/lfs", LFS.Storage.MinioConfig.BasePath)
 }
+
+func Test_getStorageConfiguration29(t *testing.T) {
+	cfg, err := NewConfigProviderFromData(`
+[repo-archive]
+STORAGE_TYPE = azureblob
+AZURE_BLOB_ACCOUNT_NAME = my_account_name
+AZURE_BLOB_ACCOUNT_KEY = my_account_key
+`)
+	assert.NoError(t, err)
+	// assert.Error(t, loadRepoArchiveFrom(cfg))
+	// FIXME: this should return error but now ini package's MapTo() doesn't check type
+	assert.NoError(t, loadRepoArchiveFrom(cfg))
+}
+
+func Test_getStorageConfiguration30(t *testing.T) {
+	cfg, err := NewConfigProviderFromData(`
+[storage.repo-archive]
+STORAGE_TYPE = azureblob
+AZURE_BLOB_ACCOUNT_NAME = my_account_name
+AZURE_BLOB_ACCOUNT_KEY = my_account_key
+`)
+	assert.NoError(t, err)
+	assert.NoError(t, loadRepoArchiveFrom(cfg))
+	assert.EqualValues(t, "my_account_name", RepoArchive.Storage.AzureBlobConfig.AccountName)
+	assert.EqualValues(t, "my_account_key", RepoArchive.Storage.AzureBlobConfig.AccountKey)
+	assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.AzureBlobConfig.BasePath)
+}
+
+func Test_getStorageConfiguration31(t *testing.T) {
+	cfg, err := NewConfigProviderFromData(`
+[storage]
+STORAGE_TYPE = azureblob
+AZURE_BLOB_ACCOUNT_NAME = my_account_name
+AZURE_BLOB_ACCOUNT_KEY = my_account_key
+AZURE_BLOB_BASE_PATH = /prefix
+`)
+	assert.NoError(t, err)
+	assert.NoError(t, loadRepoArchiveFrom(cfg))
+	assert.EqualValues(t, "my_account_name", RepoArchive.Storage.AzureBlobConfig.AccountName)
+	assert.EqualValues(t, "my_account_key", RepoArchive.Storage.AzureBlobConfig.AccountKey)
+	assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.AzureBlobConfig.BasePath)
+
+	cfg, err = NewConfigProviderFromData(`
+[storage]
+STORAGE_TYPE = azureblob
+AZURE_BLOB_ACCOUNT_NAME = my_account_name
+AZURE_BLOB_ACCOUNT_KEY = my_account_key
+AZURE_BLOB_BASE_PATH = /prefix
+
+[lfs]
+AZURE_BLOB_BASE_PATH = /lfs
+`)
+	assert.NoError(t, err)
+	assert.NoError(t, loadLFSFrom(cfg))
+	assert.EqualValues(t, "my_account_name", LFS.Storage.AzureBlobConfig.AccountName)
+	assert.EqualValues(t, "my_account_key", LFS.Storage.AzureBlobConfig.AccountKey)
+	assert.EqualValues(t, "/lfs", LFS.Storage.AzureBlobConfig.BasePath)
+
+	cfg, err = NewConfigProviderFromData(`
+[storage]
+STORAGE_TYPE = azureblob
+AZURE_BLOB_ACCOUNT_NAME = my_account_name
+AZURE_BLOB_ACCOUNT_KEY = my_account_key
+AZURE_BLOB_BASE_PATH = /prefix
+
+[storage.lfs]
+AZURE_BLOB_BASE_PATH = /lfs
+`)
+	assert.NoError(t, err)
+	assert.NoError(t, loadLFSFrom(cfg))
+	assert.EqualValues(t, "my_account_name", LFS.Storage.AzureBlobConfig.AccountName)
+	assert.EqualValues(t, "my_account_key", LFS.Storage.AzureBlobConfig.AccountKey)
+	assert.EqualValues(t, "/lfs", LFS.Storage.AzureBlobConfig.BasePath)
+}
diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go
new file mode 100644
index 0000000000..52a7d1637e
--- /dev/null
+++ b/modules/storage/azureblob.go
@@ -0,0 +1,322 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package storage
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"net/url"
+	"os"
+	"path"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
+	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
+)
+
+var _ Object = &azureBlobObject{}
+
+type azureBlobObject struct {
+	blobClient *blob.Client
+	Context    context.Context
+	Name       string
+	Size       int64
+	ModTime    *time.Time
+	offset     int64
+}
+
+func (a *azureBlobObject) Read(p []byte) (int, error) {
+	// TODO: improve the performance, we can implement another interface, maybe implement io.WriteTo
+	if a.offset >= a.Size {
+		return 0, io.EOF
+	}
+	count := min(int64(len(p)), a.Size-a.offset)
+
+	res, err := a.blobClient.DownloadBuffer(a.Context, p, &blob.DownloadBufferOptions{
+		Range: blob.HTTPRange{
+			Offset: a.offset,
+			Count:  count,
+		},
+	})
+	if err != nil {
+		return 0, convertAzureBlobErr(err)
+	}
+	a.offset += res
+
+	return int(res), nil
+}
+
+func (a *azureBlobObject) Close() error {
+	a.offset = 0
+	return nil
+}
+
+func (a *azureBlobObject) Seek(offset int64, whence int) (int64, error) {
+	switch whence {
+	case io.SeekStart:
+	case io.SeekCurrent:
+		offset += a.offset
+	case io.SeekEnd:
+		offset = a.Size - offset
+	default:
+		return 0, errors.New("Seek: invalid whence")
+	}
+
+	if offset > a.Size {
+		return 0, errors.New("Seek: invalid offset")
+	} else if offset < 0 {
+		return 0, errors.New("Seek: invalid offset")
+	}
+	a.offset = offset
+	return a.offset, nil
+}
+
+func (a *azureBlobObject) Stat() (os.FileInfo, error) {
+	return &azureBlobFileInfo{
+		a.Name,
+		a.Size,
+		*a.ModTime,
+	}, nil
+}
+
+var _ ObjectStorage = &AzureBlobStorage{}
+
+// AzureStorage returns a azure blob storage
+type AzureBlobStorage struct {
+	cfg        *setting.AzureBlobStorageConfig
+	ctx        context.Context
+	credential *azblob.SharedKeyCredential
+	client     *azblob.Client
+}
+
+func convertAzureBlobErr(err error) error {
+	if err == nil {
+		return nil
+	}
+
+	if bloberror.HasCode(err, bloberror.BlobNotFound) {
+		return os.ErrNotExist
+	}
+	var respErr *azcore.ResponseError
+	if !errors.As(err, &respErr) {
+		return err
+	}
+	return fmt.Errorf(respErr.ErrorCode)
+}
+
+// NewAzureBlobStorage returns a azure blob storage
+func NewAzureBlobStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
+	config := cfg.AzureBlobConfig
+
+	log.Info("Creating Azure Blob storage at %s:%s with base path %s", config.Endpoint, config.Container, config.BasePath)
+
+	cred, err := azblob.NewSharedKeyCredential(config.AccountName, config.AccountKey)
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+	client, err := azblob.NewClientWithSharedKeyCredential(config.Endpoint, cred, &azblob.ClientOptions{})
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+
+	_, err = client.CreateContainer(ctx, config.Container, &container.CreateOptions{})
+	if err != nil {
+		// Check to see if we already own this container (which happens if you run this twice)
+		if !bloberror.HasCode(err, bloberror.ContainerAlreadyExists) {
+			return nil, convertMinioErr(err)
+		}
+	}
+
+	return &AzureBlobStorage{
+		cfg:        &config,
+		ctx:        ctx,
+		credential: cred,
+		client:     client,
+	}, nil
+}
+
+func (a *AzureBlobStorage) buildAzureBlobPath(p string) string {
+	p = util.PathJoinRelX(a.cfg.BasePath, p)
+	if p == "." || p == "/" {
+		p = "" // azure uses prefix, so path should be empty as relative path
+	}
+	return p
+}
+
+func (a *AzureBlobStorage) getObjectNameFromPath(path string) string {
+	s := strings.Split(path, "/")
+	return s[len(s)-1]
+}
+
+// Open opens a file
+func (a *AzureBlobStorage) Open(path string) (Object, error) {
+	blobClient, err := a.getBlobClient(path)
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+	res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+	return &azureBlobObject{
+		Context:    a.ctx,
+		blobClient: blobClient,
+		Name:       a.getObjectNameFromPath(path),
+		Size:       *res.ContentLength,
+		ModTime:    res.LastModified,
+	}, nil
+}
+
+// Save saves a file to azure blob storage
+func (a *AzureBlobStorage) Save(path string, r io.Reader, size int64) (int64, error) {
+	rd := util.NewCountingReader(r)
+	_, err := a.client.UploadStream(
+		a.ctx,
+		a.cfg.Container,
+		a.buildAzureBlobPath(path),
+		rd,
+		// TODO: support set block size and concurrency
+		&blockblob.UploadStreamOptions{},
+	)
+	if err != nil {
+		return 0, convertAzureBlobErr(err)
+	}
+	return int64(rd.Count()), nil
+}
+
+type azureBlobFileInfo struct {
+	name    string
+	size    int64
+	modTime time.Time
+}
+
+func (a azureBlobFileInfo) Name() string {
+	return path.Base(a.name)
+}
+
+func (a azureBlobFileInfo) Size() int64 {
+	return a.size
+}
+
+func (a azureBlobFileInfo) ModTime() time.Time {
+	return a.modTime
+}
+
+func (a azureBlobFileInfo) IsDir() bool {
+	return strings.HasSuffix(a.name, "/")
+}
+
+func (a azureBlobFileInfo) Mode() os.FileMode {
+	return os.ModePerm
+}
+
+func (a azureBlobFileInfo) Sys() any {
+	return nil
+}
+
+// Stat returns the stat information of the object
+func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) {
+	blobClient, err := a.getBlobClient(path)
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+	res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+	s := strings.Split(path, "/")
+	return &azureBlobFileInfo{
+		s[len(s)-1],
+		*res.ContentLength,
+		*res.LastModified,
+	}, nil
+}
+
+// Delete delete a file
+func (a *AzureBlobStorage) Delete(path string) error {
+	blobClient, err := a.getBlobClient(path)
+	if err != nil {
+		return convertAzureBlobErr(err)
+	}
+	_, err = blobClient.Delete(a.ctx, nil)
+	return convertAzureBlobErr(err)
+}
+
+// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
+func (a *AzureBlobStorage) URL(path, name string) (*url.URL, error) {
+	blobClient, err := a.getBlobClient(path)
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+
+	startTime := time.Now()
+	u, err := blobClient.GetSASURL(sas.BlobPermissions{
+		Read: true,
+	}, time.Now().Add(5*time.Minute), &blob.GetSASURLOptions{
+		StartTime: &startTime,
+	})
+	if err != nil {
+		return nil, convertAzureBlobErr(err)
+	}
+
+	return url.Parse(u)
+}
+
+// IterateObjects iterates across the objects in the azureblobstorage
+func (a *AzureBlobStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error {
+	dirName = a.buildAzureBlobPath(dirName)
+	if dirName != "" {
+		dirName += "/"
+	}
+	pager := a.client.NewListBlobsFlatPager(a.cfg.Container, &container.ListBlobsFlatOptions{
+		Prefix: &dirName,
+	})
+	for pager.More() {
+		resp, err := pager.NextPage(a.ctx)
+		if err != nil {
+			return convertAzureBlobErr(err)
+		}
+		for _, object := range resp.Segment.BlobItems {
+			blobClient, err := a.getBlobClient(*object.Name)
+			if err != nil {
+				return convertAzureBlobErr(err)
+			}
+			object := &azureBlobObject{
+				Context:    a.ctx,
+				blobClient: blobClient,
+				Name:       *object.Name,
+				Size:       *object.Properties.ContentLength,
+				ModTime:    object.Properties.LastModified,
+			}
+			if err := func(object *azureBlobObject, fn func(path string, obj Object) error) error {
+				defer object.Close()
+				return fn(strings.TrimPrefix(object.Name, a.cfg.BasePath), object)
+			}(object, fn); err != nil {
+				return convertAzureBlobErr(err)
+			}
+		}
+	}
+	return nil
+}
+
+// Delete delete a file
+func (a *AzureBlobStorage) getBlobClient(path string) (*blob.Client, error) {
+	return a.client.ServiceClient().NewContainerClient(a.cfg.Container).NewBlobClient(a.buildAzureBlobPath(path)), nil
+}
+
+func init() {
+	RegisterStorageType(setting.AzureBlobStorageType, NewAzureBlobStorage)
+}
diff --git a/modules/storage/azureblob_test.go b/modules/storage/azureblob_test.go
new file mode 100644
index 0000000000..604870cb98
--- /dev/null
+++ b/modules/storage/azureblob_test.go
@@ -0,0 +1,56 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package storage
+
+import (
+	"os"
+	"testing"
+
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAzureBlobStorageIterator(t *testing.T) {
+	if os.Getenv("CI") == "" {
+		t.Skip("azureBlobStorage not present outside of CI")
+		return
+	}
+	testStorageIterator(t, setting.AzureBlobStorageType, &setting.Storage{
+		AzureBlobConfig: setting.AzureBlobStorageConfig{
+			// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url
+			Endpoint: "http://devstoreaccount1.azurite.local:10000",
+			// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key
+			AccountName: "devstoreaccount1",
+			AccountKey:  "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
+			Container:   "test",
+		},
+	})
+}
+
+func TestAzureBlobStoragePath(t *testing.T) {
+	m := &AzureBlobStorage{cfg: &setting.AzureBlobStorageConfig{BasePath: ""}}
+	assert.Equal(t, "", m.buildAzureBlobPath("/"))
+	assert.Equal(t, "", m.buildAzureBlobPath("."))
+	assert.Equal(t, "a", m.buildAzureBlobPath("/a"))
+	assert.Equal(t, "a/b", m.buildAzureBlobPath("/a/b/"))
+
+	m = &AzureBlobStorage{cfg: &setting.AzureBlobStorageConfig{BasePath: "/"}}
+	assert.Equal(t, "", m.buildAzureBlobPath("/"))
+	assert.Equal(t, "", m.buildAzureBlobPath("."))
+	assert.Equal(t, "a", m.buildAzureBlobPath("/a"))
+	assert.Equal(t, "a/b", m.buildAzureBlobPath("/a/b/"))
+
+	m = &AzureBlobStorage{cfg: &setting.AzureBlobStorageConfig{BasePath: "/base"}}
+	assert.Equal(t, "base", m.buildAzureBlobPath("/"))
+	assert.Equal(t, "base", m.buildAzureBlobPath("."))
+	assert.Equal(t, "base/a", m.buildAzureBlobPath("/a"))
+	assert.Equal(t, "base/a/b", m.buildAzureBlobPath("/a/b/"))
+
+	m = &AzureBlobStorage{cfg: &setting.AzureBlobStorageConfig{BasePath: "/base/"}}
+	assert.Equal(t, "base", m.buildAzureBlobPath("/"))
+	assert.Equal(t, "base", m.buildAzureBlobPath("."))
+	assert.Equal(t, "base/a", m.buildAzureBlobPath("/a"))
+	assert.Equal(t, "base/a/b", m.buildAzureBlobPath("/a/b/"))
+}
diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go
index ad11046dd6..6eb03c4a45 100644
--- a/modules/storage/minio_test.go
+++ b/modules/storage/minio_test.go
@@ -23,7 +23,7 @@ func TestMinioStorageIterator(t *testing.T) {
 	}
 	testStorageIterator(t, setting.MinioStorageType, &setting.Storage{
 		MinioConfig: setting.MinioStorageConfig{
-			Endpoint:        "127.0.0.1:9000",
+			Endpoint:        "minio:9000",
 			AccessKeyID:     "123456",
 			SecretAccessKey: "12345678",
 			Bucket:          "gitea",
diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go
index 5e3e9c7dba..7edde558f3 100644
--- a/modules/storage/storage_test.go
+++ b/modules/storage/storage_test.go
@@ -35,6 +35,7 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) {
 		"b":           {"b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt"},
 		"":            {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt", "ab/1.txt"},
 		"/":           {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt", "ab/1.txt"},
+		".":           {"a/1.txt", "b/1.txt", "b/2.txt", "b/3.txt", "b/x 4.txt", "ab/1.txt"},
 		"a/b/../../a": {"a/1.txt"},
 	}
 	for dir, expected := range expectedList {
diff --git a/modules/util/io.go b/modules/util/io.go
index 1559b019a0..eb200c9f9a 100644
--- a/modules/util/io.go
+++ b/modules/util/io.go
@@ -76,3 +76,24 @@ func IsEmptyReader(r io.Reader) (err error) {
 		}
 	}
 }
+
+type CountingReader struct {
+	io.Reader
+	n int
+}
+
+var _ io.Reader = &CountingReader{}
+
+func (w *CountingReader) Count() int {
+	return w.n
+}
+
+func (w *CountingReader) Read(p []byte) (int, error) {
+	n, err := w.Reader.Read(p)
+	w.n += n
+	return n, err
+}
+
+func NewCountingReader(rd io.Reader) *CountingReader {
+	return &CountingReader{Reader: rd}
+}
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
index 35e3ee6906..16af957d0f 100644
--- a/routers/api/actions/artifacts.go
+++ b/routers/api/actions/artifacts.go
@@ -428,7 +428,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
 	var items []downloadArtifactResponseItem
 	for _, artifact := range artifacts {
 		var downloadURL string
-		if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+		if setting.Actions.ArtifactStorage.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)
diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go
index 3a81724b3a..bba8ec5f94 100644
--- a/routers/api/actions/artifacts_chunks.go
+++ b/routers/api/actions/artifacts_chunks.go
@@ -55,7 +55,7 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
 		}
 	}
 	if writtenSize != contentSize {
-		checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size"))
+		checkErr = errors.Join(checkErr, fmt.Errorf("writtenSize %d not match contentSize %d", writtenSize, contentSize))
 	}
 	if checkErr != nil {
 		if err := st.Delete(storagePath); err != nil {
diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go
index dde9caf4f2..2ace9f915f 100644
--- a/routers/api/actions/artifactsv4.go
+++ b/routers/api/actions/artifactsv4.go
@@ -448,7 +448,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
 
 	respData := GetSignedArtifactURLResponse{}
 
-	if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+	if setting.Actions.ArtifactStorage.ServeDirect() {
 		u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath)
 		if u != nil && err == nil {
 			respData.SignedUrl = u.String()
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 979f5f30b9..6ecdc1ff67 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -201,7 +201,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
 		return
 	}
 
-	if setting.LFS.Storage.MinioConfig.ServeDirect {
+	if setting.LFS.Storage.ServeDirect() {
 		// If we have a signed url (S3, object storage), redirect to this directly.
 		u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
 		if u != nil && err == nil {
@@ -326,7 +326,7 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model.
 		archiver.CommitID, archiver.CommitID))
 
 	rPath := archiver.RelativePath()
-	if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
+	if setting.RepoArchive.Storage.ServeDirect() {
 		// If we have a signed url (S3, object storage), redirect to this directly.
 		u, err := storage.RepoArchives.URL(rPath, downloadName)
 		if u != nil && err == nil {
diff --git a/routers/web/base.go b/routers/web/base.go
index 78dde57fa6..c44233f957 100644
--- a/routers/web/base.go
+++ b/routers/web/base.go
@@ -23,7 +23,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
 	prefix = strings.Trim(prefix, "/")
 	funcInfo := routing.GetFuncInfo(storageHandler, prefix)
 
-	if storageSetting.MinioConfig.ServeDirect {
+	if storageSetting.ServeDirect() {
 		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 			if req.Method != "GET" && req.Method != "HEAD" {
 				http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 12909bddd5..7cc12c90e6 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -625,7 +625,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
 	// 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 {
+		if setting.Actions.ArtifactStorage.ServeDirect() {
 			u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath)
 			if u != nil && err == nil {
 				ctx.Redirect(u.String())
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index f0c5622aec..6437c39a57 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -127,7 +127,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
 		return
 	}
 
-	if setting.Attachment.Storage.MinioConfig.ServeDirect {
+	if setting.Attachment.Storage.ServeDirect() {
 		// If we have a signed url (S3, object storage), redirect to this directly.
 		u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name)
 
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index c4a8baecca..802e8e6a62 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -53,8 +53,8 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
 			return nil
 		}
 
-		if setting.LFS.Storage.MinioConfig.ServeDirect {
-			// If we have a signed url (S3, object storage), redirect to this directly.
+		if setting.LFS.Storage.ServeDirect() {
+			// If we have a signed url (S3, object storage, blob storage), redirect to this directly.
 			u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
 			if u != nil && err == nil {
 				ctx.Redirect(u.String())
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index f54b35c3e0..5a74971827 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -491,7 +491,7 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
 		archiver.CommitID, archiver.CommitID))
 
 	rPath := archiver.RelativePath()
-	if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
+	if setting.RepoArchive.Storage.ServeDirect() {
 		// If we have a signed url (S3, object storage), redirect to this directly.
 		u, err := storage.RepoArchives.URL(rPath, downloadName)
 		if u != nil && err == nil {
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 706be0d080..2e330aa1a4 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -453,7 +453,7 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa
 
 		if download {
 			var link *lfs_module.Link
-			if setting.LFS.Storage.MinioConfig.ServeDirect {
+			if setting.LFS.Storage.ServeDirect() {
 				// If we have a signed url (S3, object storage), redirect to this directly.
 				u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid)
 				if u != nil && err == nil {
diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go
index 1cbae599af..baa8dd66c8 100644
--- a/tests/integration/api_packages_generic_test.go
+++ b/tests/integration/api_packages_generic_test.go
@@ -144,18 +144,29 @@ func TestPackageGeneric(t *testing.T) {
 		t.Run("ServeDirect", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
 
-			if setting.Packages.Storage.Type != setting.MinioStorageType {
-				t.Skip("Test skipped for non-Minio-storage.")
+			if setting.Packages.Storage.Type != setting.MinioStorageType && setting.Packages.Storage.Type != setting.AzureBlobStorageType {
+				t.Skip("Test skipped for non-Minio-storage and non-AzureBlob-storage.")
 				return
 			}
 
-			if !setting.Packages.Storage.MinioConfig.ServeDirect {
-				old := setting.Packages.Storage.MinioConfig.ServeDirect
-				defer func() {
-					setting.Packages.Storage.MinioConfig.ServeDirect = old
-				}()
+			if setting.Packages.Storage.Type == setting.MinioStorageType {
+				if !setting.Packages.Storage.MinioConfig.ServeDirect {
+					old := setting.Packages.Storage.MinioConfig.ServeDirect
+					defer func() {
+						setting.Packages.Storage.MinioConfig.ServeDirect = old
+					}()
 
-				setting.Packages.Storage.MinioConfig.ServeDirect = true
+					setting.Packages.Storage.MinioConfig.ServeDirect = true
+				}
+			} else if setting.Packages.Storage.Type == setting.AzureBlobStorageType {
+				if !setting.Packages.Storage.AzureBlobConfig.ServeDirect {
+					old := setting.Packages.Storage.AzureBlobConfig.ServeDirect
+					defer func() {
+						setting.Packages.Storage.AzureBlobConfig.ServeDirect = old
+					}()
+
+					setting.Packages.Storage.AzureBlobConfig.ServeDirect = true
+				}
 			}
 
 			req := NewRequest(t, "GET", url+"/"+filename)
@@ -168,7 +179,7 @@ func TestPackageGeneric(t *testing.T) {
 
 			resp2, err := (&http.Client{}).Get(location)
 			assert.NoError(t, err)
-			assert.Equal(t, http.StatusOK, resp2.StatusCode)
+			assert.Equal(t, http.StatusOK, resp2.StatusCode, location)
 
 			body, err := io.ReadAll(resp2.Body)
 			assert.NoError(t, err)
diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl
index 07997f62ed..77c969e813 100644
--- a/tests/mssql.ini.tmpl
+++ b/tests/mssql.ini.tmpl
@@ -53,9 +53,6 @@ APP_DATA_PATH    = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data
 BUILTIN_SSH_SERVER_USER = git
 SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
 
-[attachment]
-PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/attachments
-
 [mailer]
 ENABLED = true
 PROTOCOL = dummy
@@ -102,8 +99,13 @@ SECRET_KEY     = 9pCviYTWSb
 INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
 DISABLE_QUERY_AUTH_TOKEN = true
 
-[lfs]
-PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/lfs
+[storage]
+STORAGE_TYPE = azureblob
+AZURE_BLOB_ENDPOINT = http://devstoreaccount1.azurite.local:10000
+AZURE_BLOB_ACCOUNT_NAME = devstoreaccount1
+AZURE_BLOB_ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
+AZURE_BLOB_CONTAINER = gitea
+SERVE_DIRECT = false
 
 [packages]
 ENABLED = true
diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl
index 486cfc945c..6b54f790c5 100644
--- a/tests/pgsql.ini.tmpl
+++ b/tests/pgsql.ini.tmpl
@@ -54,9 +54,6 @@ APP_DATA_PATH    = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data
 BUILTIN_SSH_SERVER_USER = git
 SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
 
-[attachment]
-PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/attachments
-
 [mailer]
 ENABLED = true
 PROTOCOL = dummy

From 1137a0357eb1e35a046e86a7277594154d0f6c85 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 31 May 2024 09:58:41 +0800
Subject: [PATCH 071/131] Fix branch order (#31174)

Fix #31172

The original order or the default order should not be ignored even if we
have an is_deleted order.
---
 models/git/branch_list.go | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/models/git/branch_list.go b/models/git/branch_list.go
index 5c887461d5..25e84526d2 100644
--- a/models/git/branch_list.go
+++ b/models/git/branch_list.go
@@ -107,17 +107,13 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
 
 func (opts FindBranchOptions) ToOrders() string {
 	orderBy := opts.OrderBy
-	if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end
-		if orderBy != "" {
-			orderBy += ", "
-		}
-		orderBy += "is_deleted ASC"
-	}
 	if orderBy == "" {
 		// the commit_time might be the same, so add the "name" to make sure the order is stable
-		return "commit_time DESC, name ASC"
+		orderBy = "commit_time DESC, name ASC"
+	}
+	if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the beginning
+		orderBy = "is_deleted ASC, " + orderBy
 	}
-
 	return orderBy
 }
 

From 572fa55fbcc2cb9418b4f7b981a7c80a11899276 Mon Sep 17 00:00:00 2001
From: Jason Song <i@wolfogre.com>
Date: Fri, 31 May 2024 10:30:02 +0800
Subject: [PATCH 072/131] Drop `IDOrderDesc` for listing Actions task and
 always order by `id DESC` (#31150)

Close #31066

Just follow what `FindRunOptions` and `FindScheduleOptions` do.
---
 models/actions/task_list.go           | 6 +-----
 routers/web/shared/actions/runners.go | 5 ++---
 2 files changed, 3 insertions(+), 8 deletions(-)

diff --git a/models/actions/task_list.go b/models/actions/task_list.go
index 5e17f91441..df4b43c5ef 100644
--- a/models/actions/task_list.go
+++ b/models/actions/task_list.go
@@ -54,7 +54,6 @@ type FindTaskOptions struct {
 	UpdatedBefore timeutil.TimeStamp
 	StartedBefore timeutil.TimeStamp
 	RunnerID      int64
-	IDOrderDesc   bool
 }
 
 func (opts FindTaskOptions) ToConds() builder.Cond {
@@ -84,8 +83,5 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
 }
 
 func (opts FindTaskOptions) ToOrders() string {
-	if opts.IDOrderDesc {
-		return "`id` DESC"
-	}
-	return ""
+	return "`id` DESC"
 }
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index 34b7969442..f38933226b 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -79,9 +79,8 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
 			Page:     page,
 			PageSize: 30,
 		},
-		Status:      actions_model.StatusUnknown, // Unknown means all
-		IDOrderDesc: true,
-		RunnerID:    runner.ID,
+		Status:   actions_model.StatusUnknown, // Unknown means all
+		RunnerID: runner.ID,
 	}
 
 	tasks, count, err := db.FindAndCount[actions_model.ActionTask](ctx, opts)

From 972f807ee7d0643b93a776d362ecefc3d5433048 Mon Sep 17 00:00:00 2001
From: TheBrokenRail <17478432+TheBrokenRail@users.noreply.github.com>
Date: Fri, 31 May 2024 07:41:44 -0400
Subject: [PATCH 073/131] Fix URL In Gitea Actions Badge Docs (#31191)

The example URL given in the documentation leads to a 404.

For instance,
`https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}`
translates to
`https://gitea.thebrokenrail.com/minecraft-pi-reborn/minecraft-pi-reborn/actions/workflows/build.yml`,
which is a 404.

I had to check the [linked GitHub
docs](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge)
to learn that you have to add `/badge.svg` to the URL.

Example:
https://gitea.thebrokenrail.com/minecraft-pi-reborn/minecraft-pi-reborn/actions/workflows/build.yml/badge.svg
---
 docs/content/usage/actions/badge.en-us.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/content/usage/actions/badge.en-us.md b/docs/content/usage/actions/badge.en-us.md
index de7a34f4e6..57e5d9d3a1 100644
--- a/docs/content/usage/actions/badge.en-us.md
+++ b/docs/content/usage/actions/badge.en-us.md
@@ -25,7 +25,7 @@ It is designed to be compatible with [GitHub Actions workflow badge](https://doc
 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}
+https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}/badge.svg?branch={branch}&event={event}
 ```
 
 - `{owner}`: The owner of the repository.

From 352a2cae247afa254241f113c5c22b9351f116b9 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 31 May 2024 20:10:11 +0800
Subject: [PATCH 074/131] Performance improvements for pull request list API
 (#30490)

Fix #30483

---------

Co-authored-by: yp05327 <576951401@qq.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 models/issues/assignees.go      |  12 ++--
 models/issues/comment_list.go   |  12 ++--
 models/issues/issue.go          |  96 ++++++++++++++++-----------
 models/issues/issue_label.go    |   6 +-
 models/issues/issue_list.go     |  47 ++++++++------
 models/issues/pull.go           |  13 ++--
 models/issues/pull_list.go      | 111 ++++++++++++++++++++++----------
 models/user/user.go             |   4 ++
 routers/api/v1/repo/pull.go     |  48 +++++++++-----
 services/convert/issue.go       |   9 ++-
 services/convert/pull.go        |  12 +++-
 services/issue/assignee_test.go |   3 +-
 12 files changed, 243 insertions(+), 130 deletions(-)

diff --git a/models/issues/assignees.go b/models/issues/assignees.go
index 30234be07a..efd992cda2 100644
--- a/models/issues/assignees.go
+++ b/models/issues/assignees.go
@@ -27,23 +27,27 @@ func init() {
 
 // LoadAssignees load assignees of this issue.
 func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
+	if issue.isAssigneeLoaded || len(issue.Assignees) > 0 {
+		return nil
+	}
+
 	// Reset maybe preexisting assignees
 	issue.Assignees = []*user_model.User{}
 	issue.Assignee = nil
 
-	err = db.GetEngine(ctx).Table("`user`").
+	if err = db.GetEngine(ctx).Table("`user`").
 		Join("INNER", "issue_assignees", "assignee_id = `user`.id").
 		Where("issue_assignees.issue_id = ?", issue.ID).
-		Find(&issue.Assignees)
-	if err != nil {
+		Find(&issue.Assignees); err != nil {
 		return err
 	}
 
+	issue.isAssigneeLoaded = true
 	// Check if we have at least one assignee and if yes put it in as `Assignee`
 	if len(issue.Assignees) > 0 {
 		issue.Assignee = issue.Assignees[0]
 	}
-	return err
+	return nil
 }
 
 // GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index 370b5396e0..6b4ad80eed 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -16,19 +16,17 @@ import (
 // CommentList defines a list of comments
 type CommentList []*Comment
 
-func (comments CommentList) getPosterIDs() []int64 {
-	return container.FilterSlice(comments, func(c *Comment) (int64, bool) {
-		return c.PosterID, c.PosterID > 0
-	})
-}
-
 // LoadPosters loads posters
 func (comments CommentList) LoadPosters(ctx context.Context) error {
 	if len(comments) == 0 {
 		return nil
 	}
 
-	posterMaps, err := getPosters(ctx, comments.getPosterIDs())
+	posterIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
+		return c.PosterID, c.Poster == nil && c.PosterID > 0
+	})
+
+	posterMaps, err := getPostersByIDs(ctx, posterIDs)
 	if err != nil {
 		return err
 	}
diff --git a/models/issues/issue.go b/models/issues/issue.go
index aad855522d..40462ed09d 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -98,32 +98,35 @@ var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already
 
 // Issue represents an issue or pull request of repository.
 type Issue struct {
-	ID               int64                  `xorm:"pk autoincr"`
-	RepoID           int64                  `xorm:"INDEX UNIQUE(repo_index)"`
-	Repo             *repo_model.Repository `xorm:"-"`
-	Index            int64                  `xorm:"UNIQUE(repo_index)"` // Index in one repository.
-	PosterID         int64                  `xorm:"INDEX"`
-	Poster           *user_model.User       `xorm:"-"`
-	OriginalAuthor   string
-	OriginalAuthorID int64                  `xorm:"index"`
-	Title            string                 `xorm:"name"`
-	Content          string                 `xorm:"LONGTEXT"`
-	RenderedContent  template.HTML          `xorm:"-"`
-	ContentVersion   int                    `xorm:"NOT NULL DEFAULT 0"`
-	Labels           []*Label               `xorm:"-"`
-	MilestoneID      int64                  `xorm:"INDEX"`
-	Milestone        *Milestone             `xorm:"-"`
-	Project          *project_model.Project `xorm:"-"`
-	Priority         int
-	AssigneeID       int64            `xorm:"-"`
-	Assignee         *user_model.User `xorm:"-"`
-	IsClosed         bool             `xorm:"INDEX"`
-	IsRead           bool             `xorm:"-"`
-	IsPull           bool             `xorm:"INDEX"` // Indicates whether is a pull request or not.
-	PullRequest      *PullRequest     `xorm:"-"`
-	NumComments      int
-	Ref              string
-	PinOrder         int `xorm:"DEFAULT 0"`
+	ID                int64                  `xorm:"pk autoincr"`
+	RepoID            int64                  `xorm:"INDEX UNIQUE(repo_index)"`
+	Repo              *repo_model.Repository `xorm:"-"`
+	Index             int64                  `xorm:"UNIQUE(repo_index)"` // Index in one repository.
+	PosterID          int64                  `xorm:"INDEX"`
+	Poster            *user_model.User       `xorm:"-"`
+	OriginalAuthor    string
+	OriginalAuthorID  int64                  `xorm:"index"`
+	Title             string                 `xorm:"name"`
+	Content           string                 `xorm:"LONGTEXT"`
+	RenderedContent   template.HTML          `xorm:"-"`
+	ContentVersion    int                    `xorm:"NOT NULL DEFAULT 0"`
+	Labels            []*Label               `xorm:"-"`
+	isLabelsLoaded    bool                   `xorm:"-"`
+	MilestoneID       int64                  `xorm:"INDEX"`
+	Milestone         *Milestone             `xorm:"-"`
+	isMilestoneLoaded bool                   `xorm:"-"`
+	Project           *project_model.Project `xorm:"-"`
+	Priority          int
+	AssigneeID        int64            `xorm:"-"`
+	Assignee          *user_model.User `xorm:"-"`
+	isAssigneeLoaded  bool             `xorm:"-"`
+	IsClosed          bool             `xorm:"INDEX"`
+	IsRead            bool             `xorm:"-"`
+	IsPull            bool             `xorm:"INDEX"` // Indicates whether is a pull request or not.
+	PullRequest       *PullRequest     `xorm:"-"`
+	NumComments       int
+	Ref               string
+	PinOrder          int `xorm:"DEFAULT 0"`
 
 	DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
 
@@ -131,11 +134,12 @@ type Issue struct {
 	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 	ClosedUnix  timeutil.TimeStamp `xorm:"INDEX"`
 
-	Attachments      []*repo_model.Attachment `xorm:"-"`
-	Comments         CommentList              `xorm:"-"`
-	Reactions        ReactionList             `xorm:"-"`
-	TotalTrackedTime int64                    `xorm:"-"`
-	Assignees        []*user_model.User       `xorm:"-"`
+	Attachments         []*repo_model.Attachment `xorm:"-"`
+	isAttachmentsLoaded bool                     `xorm:"-"`
+	Comments            CommentList              `xorm:"-"`
+	Reactions           ReactionList             `xorm:"-"`
+	TotalTrackedTime    int64                    `xorm:"-"`
+	Assignees           []*user_model.User       `xorm:"-"`
 
 	// IsLocked limits commenting abilities to users on an issue
 	// with write access
@@ -187,6 +191,19 @@ func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
 	return nil
 }
 
+func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {
+	if issue.isAttachmentsLoaded || issue.Attachments != nil {
+		return nil
+	}
+
+	issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
+	if err != nil {
+		return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
+	}
+	issue.isAttachmentsLoaded = true
+	return nil
+}
+
 // IsTimetrackerEnabled returns true if the repo enables timetracking
 func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
 	if err := issue.LoadRepo(ctx); err != nil {
@@ -287,11 +304,12 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
 
 // LoadMilestone load milestone of this issue.
 func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
-	if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
+	if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
 		issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
 		if err != nil && !IsErrMilestoneNotExist(err) {
 			return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
 		}
+		issue.isMilestoneLoaded = true
 	}
 	return nil
 }
@@ -327,11 +345,8 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
 		return err
 	}
 
-	if issue.Attachments == nil {
-		issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
-		if err != nil {
-			return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
-		}
+	if err = issue.LoadAttachments(ctx); err != nil {
+		return err
 	}
 
 	if err = issue.loadComments(ctx); err != nil {
@@ -350,6 +365,13 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
 	return issue.loadReactions(ctx)
 }
 
+func (issue *Issue) ResetAttributesLoaded() {
+	issue.isLabelsLoaded = false
+	issue.isMilestoneLoaded = false
+	issue.isAttachmentsLoaded = false
+	issue.isAssigneeLoaded = false
+}
+
 // GetIsRead load the `IsRead` field of the issue
 func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
 	issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
diff --git a/models/issues/issue_label.go b/models/issues/issue_label.go
index 733f1043b0..10fc821454 100644
--- a/models/issues/issue_label.go
+++ b/models/issues/issue_label.go
@@ -111,6 +111,7 @@ func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m
 		return err
 	}
 
+	issue.isLabelsLoaded = false
 	issue.Labels = nil
 	if err = issue.LoadLabels(ctx); err != nil {
 		return err
@@ -160,6 +161,8 @@ func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
 		return err
 	}
 
+	// reload all labels
+	issue.isLabelsLoaded = false
 	issue.Labels = nil
 	if err = issue.LoadLabels(ctx); err != nil {
 		return err
@@ -325,11 +328,12 @@ func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
 
 // LoadLabels loads labels
 func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
-	if issue.Labels == nil && issue.ID != 0 {
+	if !issue.isLabelsLoaded && issue.Labels == nil && issue.ID != 0 {
 		issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
 		if err != nil {
 			return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
 		}
+		issue.isLabelsLoaded = true
 	}
 	return nil
 }
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index f8ee271a6b..0dd37a04df 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -72,18 +72,16 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
 	return repo_model.ValuesRepository(repoMaps), nil
 }
 
-func (issues IssueList) getPosterIDs() []int64 {
-	return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
-		return issue.PosterID, true
-	})
-}
-
-func (issues IssueList) loadPosters(ctx context.Context) error {
+func (issues IssueList) LoadPosters(ctx context.Context) error {
 	if len(issues) == 0 {
 		return nil
 	}
 
-	posterMaps, err := getPosters(ctx, issues.getPosterIDs())
+	posterIDs := container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
+		return issue.PosterID, issue.Poster == nil && issue.PosterID > 0
+	})
+
+	posterMaps, err := getPostersByIDs(ctx, posterIDs)
 	if err != nil {
 		return err
 	}
@@ -94,7 +92,7 @@ func (issues IssueList) loadPosters(ctx context.Context) error {
 	return nil
 }
 
-func getPosters(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
+func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
 	posterMaps := make(map[int64]*user_model.User, len(posterIDs))
 	left := len(posterIDs)
 	for left > 0 {
@@ -136,7 +134,7 @@ func (issues IssueList) getIssueIDs() []int64 {
 	return ids
 }
 
-func (issues IssueList) loadLabels(ctx context.Context) error {
+func (issues IssueList) LoadLabels(ctx context.Context) error {
 	if len(issues) == 0 {
 		return nil
 	}
@@ -168,7 +166,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
 			err = rows.Scan(&labelIssue)
 			if err != nil {
 				if err1 := rows.Close(); err1 != nil {
-					return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
+					return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
 				}
 				return err
 			}
@@ -177,7 +175,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
 		// When there are no rows left and we try to close it.
 		// Since that is not relevant for us, we can safely ignore it.
 		if err1 := rows.Close(); err1 != nil {
-			return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
+			return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
 		}
 		left -= limit
 		issueIDs = issueIDs[limit:]
@@ -185,6 +183,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
 
 	for _, issue := range issues {
 		issue.Labels = issueLabels[issue.ID]
+		issue.isLabelsLoaded = true
 	}
 	return nil
 }
@@ -195,7 +194,7 @@ func (issues IssueList) getMilestoneIDs() []int64 {
 	})
 }
 
-func (issues IssueList) loadMilestones(ctx context.Context) error {
+func (issues IssueList) LoadMilestones(ctx context.Context) error {
 	milestoneIDs := issues.getMilestoneIDs()
 	if len(milestoneIDs) == 0 {
 		return nil
@@ -220,6 +219,7 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
 
 	for _, issue := range issues {
 		issue.Milestone = milestoneMaps[issue.MilestoneID]
+		issue.isMilestoneLoaded = true
 	}
 	return nil
 }
@@ -263,7 +263,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
 	return nil
 }
 
-func (issues IssueList) loadAssignees(ctx context.Context) error {
+func (issues IssueList) LoadAssignees(ctx context.Context) error {
 	if len(issues) == 0 {
 		return nil
 	}
@@ -310,6 +310,10 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
 
 	for _, issue := range issues {
 		issue.Assignees = assignees[issue.ID]
+		if len(issue.Assignees) > 0 {
+			issue.Assignee = issue.Assignees[0]
+		}
+		issue.isAssigneeLoaded = true
 	}
 	return nil
 }
@@ -413,6 +417,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
 
 	for _, issue := range issues {
 		issue.Attachments = attachments[issue.ID]
+		issue.isAttachmentsLoaded = true
 	}
 	return nil
 }
@@ -538,23 +543,23 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error {
 		return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
 	}
 
-	if err := issues.loadPosters(ctx); err != nil {
-		return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
+	if err := issues.LoadPosters(ctx); err != nil {
+		return fmt.Errorf("issue.loadAttributes: LoadPosters: %w", err)
 	}
 
-	if err := issues.loadLabels(ctx); err != nil {
-		return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
+	if err := issues.LoadLabels(ctx); err != nil {
+		return fmt.Errorf("issue.loadAttributes: LoadLabels: %w", err)
 	}
 
-	if err := issues.loadMilestones(ctx); err != nil {
-		return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
+	if err := issues.LoadMilestones(ctx); err != nil {
+		return fmt.Errorf("issue.loadAttributes: LoadMilestones: %w", err)
 	}
 
 	if err := issues.LoadProjects(ctx); err != nil {
 		return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
 	}
 
-	if err := issues.loadAssignees(ctx); err != nil {
+	if err := issues.LoadAssignees(ctx); err != nil {
 		return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
 	}
 
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 014fcd9fd0..ef49a51045 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -159,10 +159,11 @@ type PullRequest struct {
 
 	ChangedProtectedFiles []string `xorm:"TEXT JSON"`
 
-	IssueID            int64  `xorm:"INDEX"`
-	Issue              *Issue `xorm:"-"`
-	Index              int64
-	RequestedReviewers []*user_model.User `xorm:"-"`
+	IssueID                    int64  `xorm:"INDEX"`
+	Issue                      *Issue `xorm:"-"`
+	Index                      int64
+	RequestedReviewers         []*user_model.User `xorm:"-"`
+	isRequestedReviewersLoaded bool               `xorm:"-"`
 
 	HeadRepoID          int64                  `xorm:"INDEX"`
 	HeadRepo            *repo_model.Repository `xorm:"-"`
@@ -289,7 +290,7 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
 
 // LoadRequestedReviewers loads the requested reviewers.
 func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
-	if len(pr.RequestedReviewers) > 0 {
+	if pr.isRequestedReviewersLoaded || len(pr.RequestedReviewers) > 0 {
 		return nil
 	}
 
@@ -297,10 +298,10 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-
 	if err = reviews.LoadReviewers(ctx); err != nil {
 		return err
 	}
+	pr.isRequestedReviewersLoaded = true
 	for _, review := range reviews {
 		pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
 	}
diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index b5557cad06..e8011a916f 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -9,8 +9,10 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	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/util"
 
@@ -123,7 +125,7 @@ func GetPullRequestIDsByCheckStatus(ctx context.Context, status PullRequestStatu
 }
 
 // PullRequests returns all pull requests for a base Repo by the given conditions
-func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
+func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (PullRequestList, int64, error) {
 	if opts.Page <= 0 {
 		opts.Page = 1
 	}
@@ -153,50 +155,93 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
 // PullRequestList defines a list of pull requests
 type PullRequestList []*PullRequest
 
-func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
-	if len(prs) == 0 {
-		return nil
+func (prs PullRequestList) getRepositoryIDs() []int64 {
+	repoIDs := make(container.Set[int64])
+	for _, pr := range prs {
+		if pr.BaseRepo == nil && pr.BaseRepoID > 0 {
+			repoIDs.Add(pr.BaseRepoID)
+		}
+		if pr.HeadRepo == nil && pr.HeadRepoID > 0 {
+			repoIDs.Add(pr.HeadRepoID)
+		}
 	}
+	return repoIDs.Values()
+}
 
-	// Load issues.
-	issueIDs := prs.GetIssueIDs()
-	issues := make([]*Issue, 0, len(issueIDs))
+func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
+	repoIDs := prs.getRepositoryIDs()
+	reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
 	if err := db.GetEngine(ctx).
-		Where("id > 0").
-		In("id", issueIDs).
-		Find(&issues); err != nil {
-		return fmt.Errorf("find issues: %w", err)
-	}
-
-	set := make(map[int64]*Issue)
-	for i := range issues {
-		set[issues[i].ID] = issues[i]
+		In("id", repoIDs).
+		Find(&reposMap); err != nil {
+		return fmt.Errorf("find repos: %w", err)
 	}
 	for _, pr := range prs {
-		pr.Issue = set[pr.IssueID]
-		/*
-			Old code:
-			pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
-
-			It's worth panic because it's almost impossible to happen under normal use.
-			But in integration testing, an asynchronous task could read a database that has been reset.
-			So returning an error would make more sense, let the caller has a choice to ignore it.
-		*/
-		if pr.Issue == nil {
-			return fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
+		if pr.BaseRepo == nil {
+			pr.BaseRepo = reposMap[pr.BaseRepoID]
+		}
+		if pr.HeadRepo == nil {
+			pr.HeadRepo = reposMap[pr.HeadRepoID]
+			pr.isHeadRepoLoaded = true
 		}
-		pr.Issue.PullRequest = pr
 	}
 	return nil
 }
 
+func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
+	if _, err := prs.LoadIssues(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
+	if len(prs) == 0 {
+		return nil, nil
+	}
+
+	// Load issues.
+	issueIDs := prs.GetIssueIDs()
+	issues := make(map[int64]*Issue, len(issueIDs))
+	if err := db.GetEngine(ctx).
+		In("id", issueIDs).
+		Find(&issues); err != nil {
+		return nil, fmt.Errorf("find issues: %w", err)
+	}
+
+	issueList := make(IssueList, 0, len(prs))
+	for _, pr := range prs {
+		if pr.Issue == nil {
+			pr.Issue = issues[pr.IssueID]
+			/*
+				Old code:
+				pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
+
+				It's worth panic because it's almost impossible to happen under normal use.
+				But in integration testing, an asynchronous task could read a database that has been reset.
+				So returning an error would make more sense, let the caller has a choice to ignore it.
+			*/
+			if pr.Issue == nil {
+				return nil, fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
+			}
+		}
+		pr.Issue.PullRequest = pr
+		if pr.Issue.Repo == nil {
+			pr.Issue.Repo = pr.BaseRepo
+		}
+		issueList = append(issueList, pr.Issue)
+	}
+	return issueList, nil
+}
+
 // GetIssueIDs returns all issue ids
 func (prs PullRequestList) GetIssueIDs() []int64 {
-	issueIDs := make([]int64, 0, len(prs))
-	for i := range prs {
-		issueIDs = append(issueIDs, prs[i].IssueID)
-	}
-	return issueIDs
+	return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
+		if pr.Issue == nil {
+			return pr.IssueID, pr.IssueID > 0
+		}
+		return 0, false
+	})
 }
 
 // HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
diff --git a/models/user/user.go b/models/user/user.go
index 6848d1be95..23637f4616 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -856,6 +856,10 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
 
 // GetUserByIDs returns the user objects by given IDs if exists.
 func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
+	if len(ids) == 0 {
+		return nil, nil
+	}
+
 	users := make([]*User, 0, len(ids))
 	err := db.GetEngine(ctx).In("id", ids).
 		Table("user").
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index a9aa5c4d8e..4014fe80f3 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -116,23 +116,39 @@ func ListPullRequests(ctx *context.APIContext) {
 	}
 
 	apiPrs := make([]*api.PullRequest, len(prs))
+	// NOTE: load repository first, so that issue.Repo will be filled with pr.BaseRepo
+	if err := prs.LoadRepositories(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
+		return
+	}
+	issueList, err := prs.LoadIssues(ctx)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
+		return
+	}
+
+	if err := issueList.LoadLabels(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadLabels", err)
+		return
+	}
+	if err := issueList.LoadPosters(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadPoster", err)
+		return
+	}
+	if err := issueList.LoadAttachments(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+		return
+	}
+	if err := issueList.LoadMilestones(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadMilestones", err)
+		return
+	}
+	if err := issueList.LoadAssignees(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "LoadAssignees", err)
+		return
+	}
+
 	for i := range prs {
-		if err = prs[i].LoadIssue(ctx); err != nil {
-			ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
-			return
-		}
-		if err = prs[i].LoadAttributes(ctx); err != nil {
-			ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
-			return
-		}
-		if err = prs[i].LoadBaseRepo(ctx); err != nil {
-			ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
-			return
-		}
-		if err = prs[i].LoadHeadRepo(ctx); err != nil {
-			ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
-			return
-		}
 		apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer)
 	}
 
diff --git a/services/convert/issue.go b/services/convert/issue.go
index 4fe7ef44fe..f514dc4313 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -31,15 +31,15 @@ func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.
 }
 
 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{}
-	}
 	if err := issue.LoadPoster(ctx); err != nil {
 		return &api.Issue{}
 	}
 	if err := issue.LoadRepo(ctx); err != nil {
 		return &api.Issue{}
 	}
+	if err := issue.LoadAttachments(ctx); err != nil {
+		return &api.Issue{}
+	}
 
 	apiIssue := &api.Issue{
 		ID:          issue.ID,
@@ -63,6 +63,9 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
 		}
 		apiIssue.URL = issue.APIURL(ctx)
 		apiIssue.HTMLURL = issue.HTMLURL()
+		if err := issue.LoadLabels(ctx); err != nil {
+			return &api.Issue{}
+		}
 		apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
 		apiIssue.Repo = &api.RepositoryMeta{
 			ID:       issue.Repo.ID,
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 6d95804b38..c214805ed5 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -11,6 +11,7 @@ import (
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	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"
@@ -44,7 +45,16 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 		return nil
 	}
 
-	p, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
+	var doerID int64
+	if doer != nil {
+		doerID = doer.ID
+	}
+
+	const repoDoerPermCacheKey = "repo_doer_perm_cache"
+	p, err := cache.GetWithContextCache(ctx, repoDoerPermCacheKey, fmt.Sprintf("%d_%d", pr.BaseRepoID, doerID),
+		func() (access_model.Permission, error) {
+			return access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
+		})
 	if err != nil {
 		log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
 		p.AccessMode = perm.AccessModeNone
diff --git a/services/issue/assignee_test.go b/services/issue/assignee_test.go
index da25da60ee..38d56f9d9d 100644
--- a/services/issue/assignee_test.go
+++ b/services/issue/assignee_test.go
@@ -39,7 +39,8 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Empty(t, issue.Assignees)
 
-	// Check they're gone
+	// Reload to check they're gone
+	issue.ResetAttributesLoaded()
 	assert.NoError(t, issue.LoadAssignees(db.DefaultContext))
 	assert.Empty(t, issue.Assignees)
 	assert.Empty(t, issue.Assignee)

From a4275951ba9635e9b7de6a91812b6cc9622c8c9b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 31 May 2024 21:26:01 +0800
Subject: [PATCH 075/131] Split sanitizer functions and fine-tune some tests
 (#31192)

---
 modules/markup/html_test.go                   |  16 +-
 modules/markup/renderer.go                    |   1 -
 modules/markup/sanitizer.go                   | 222 ++----------------
 modules/markup/sanitizer_custom.go            |  25 ++
 modules/markup/sanitizer_default.go           | 146 ++++++++++++
 ...izer_test.go => sanitizer_default_test.go} |  37 +--
 modules/markup/sanitizer_description.go       |  37 +++
 modules/markup/sanitizer_description_test.go  |  31 +++
 8 files changed, 270 insertions(+), 245 deletions(-)
 create mode 100644 modules/markup/sanitizer_custom.go
 create mode 100644 modules/markup/sanitizer_default.go
 rename modules/markup/{sanitizer_test.go => sanitizer_default_test.go} (72%)
 create mode 100644 modules/markup/sanitizer_description.go
 create mode 100644 modules/markup/sanitizer_description_test.go

diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 0091397768..e2d08692e4 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -169,13 +169,18 @@ func TestRender_links(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 	}
-	// Text that should be turned into URL
 
-	defaultCustom := setting.Markdown.CustomURLSchemes
+	oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
+	markup.ResetDefaultSanitizerForTesting()
+	defer func() {
+		setting.Markdown.CustomURLSchemes = oldCustomURLSchemes
+		markup.ResetDefaultSanitizerForTesting()
+		markup.CustomLinkURLSchemes(oldCustomURLSchemes)
+	}()
 	setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
-	markup.InitializeSanitizer()
 	markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
 
+	// Text that should be turned into URL
 	test(
 		"https://www.example.com",
 		`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
@@ -259,11 +264,6 @@ func TestRender_links(t *testing.T) {
 	test(
 		"ftps://gitea.com",
 		`<p>ftps://gitea.com</p>`)
-
-	// Restore previous settings
-	setting.Markdown.CustomURLSchemes = defaultCustom
-	markup.InitializeSanitizer()
-	markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
 }
 
 func TestRender_email(t *testing.T) {
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index f836f12ad3..44dedf638b 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -47,7 +47,6 @@ func Init(ph *ProcessorHelper) {
 		DefaultProcessorHelper = *ph
 	}
 
-	NewSanitizer()
 	if len(setting.Markdown.CustomURLSchemes) > 0 {
 		CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
 	}
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 570a1da248..391ddad46c 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -5,13 +5,9 @@
 package markup
 
 import (
-	"io"
-	"net/url"
 	"regexp"
 	"sync"
 
-	"code.gitea.io/gitea/modules/setting"
-
 	"github.com/microcosm-cc/bluemonday"
 )
 
@@ -21,211 +17,35 @@ type Sanitizer struct {
 	defaultPolicy     *bluemonday.Policy
 	descriptionPolicy *bluemonday.Policy
 	rendererPolicies  map[string]*bluemonday.Policy
-	init              sync.Once
+	allowAllRegex     *regexp.Regexp
 }
 
 var (
-	sanitizer     = &Sanitizer{}
-	allowAllRegex = regexp.MustCompile(".+")
+	defaultSanitizer     *Sanitizer
+	defaultSanitizerOnce sync.Once
 )
 
-// NewSanitizer initializes sanitizer with allowed attributes based on settings.
-// Multiple calls to this function will only create one instance of Sanitizer during
-// entire application lifecycle.
-func NewSanitizer() {
-	sanitizer.init.Do(func() {
-		InitializeSanitizer()
-	})
-}
-
-// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings
-func InitializeSanitizer() {
-	sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
-	sanitizer.defaultPolicy = createDefaultPolicy()
-	sanitizer.descriptionPolicy = createRepoDescriptionPolicy()
-
-	for name, renderer := range renderers {
-		sanitizerRules := renderer.SanitizerRules()
-		if len(sanitizerRules) > 0 {
-			policy := createDefaultPolicy()
-			addSanitizerRules(policy, sanitizerRules)
-			sanitizer.rendererPolicies[name] = policy
+func GetDefaultSanitizer() *Sanitizer {
+	defaultSanitizerOnce.Do(func() {
+		defaultSanitizer = &Sanitizer{
+			rendererPolicies: map[string]*bluemonday.Policy{},
+			allowAllRegex:    regexp.MustCompile(".+"),
 		}
-	}
-}
-
-func createDefaultPolicy() *bluemonday.Policy {
-	policy := bluemonday.UGCPolicy()
-
-	// 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("div")
-
-	// 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")
-
-	// For attention
-	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(`^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")
-
-	// For Chroma markdown plugin
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
-
-	// Checkboxes
-	policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
-	policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
-
-	// Custom URL-Schemes
-	if len(setting.Markdown.CustomURLSchemes) > 0 {
-		policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
-	} else {
-		policy.AllowURLSchemesMatching(allowAllRegex)
-
-		// Even if every scheme is allowed, these three are blocked for security reasons
-		disallowScheme := func(*url.URL) bool {
-			return false
-		}
-		policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
-		policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
-		policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
-	}
-
-	// Allow classes for anchors
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
-
-	// Allow classes for task lists
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
-
-	// Allow classes for org mode list item status.
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
-
-	// Allow icons
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
-
-	// 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 'color' and 'background-color' properties for the style attribute on text elements.
-	policy.AllowStyles("color", "background-color").OnElements("span", "p")
-
-	// Allow generally safe attributes
-	generalSafeAttrs := []string{
-		"abbr", "accept", "accept-charset",
-		"accesskey", "action", "align", "alt",
-		"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
-		"axis", "border", "cellpadding", "cellspacing", "char",
-		"charoff", "charset", "checked",
-		"clear", "cols", "colspan", "color",
-		"compact", "coords", "datetime", "dir",
-		"disabled", "enctype", "for", "frame",
-		"headers", "height", "hreflang",
-		"hspace", "ismap", "label", "lang",
-		"maxlength", "media", "method",
-		"multiple", "name", "nohref", "noshade",
-		"nowrap", "open", "prompt", "readonly", "rel", "rev",
-		"rows", "rowspan", "rules", "scope",
-		"selected", "shape", "size", "span",
-		"start", "summary", "tabindex", "target",
-		"title", "type", "usemap", "valign", "value",
-		"vspace", "width", "itemprop",
-	}
-
-	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", "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",
-	}
-
-	policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
-
-	policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
-
-	policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
-
-	// FIXME: Need to handle longdesc in img but there is no easy way to do it
-
-	// Custom keyword markup
-	addSanitizerRules(policy, setting.ExternalSanitizerRules)
-
-	return policy
-}
-
-// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
-// repository descriptions.
-func createRepoDescriptionPolicy() *bluemonday.Policy {
-	policy := bluemonday.NewPolicy()
-
-	// Allow italics and bold.
-	policy.AllowElements("i", "b", "em", "strong")
-
-	// Allow code.
-	policy.AllowElements("code")
-
-	// Allow links
-	policy.AllowAttrs("href", "target", "rel").OnElements("a")
-
-	// Allow classes for emojis
-	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
-	policy.AllowAttrs("aria-label").OnElements("span")
-
-	return policy
-}
-
-func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
-	for _, rule := range rules {
-		if rule.AllowDataURIImages {
-			policy.AllowDataURIImages()
-		}
-		if rule.Element != "" {
-			if rule.Regexp != nil {
-				policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
-			} else {
-				policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
+		for name, renderer := range renderers {
+			sanitizerRules := renderer.SanitizerRules()
+			if len(sanitizerRules) > 0 {
+				policy := defaultSanitizer.createDefaultPolicy()
+				defaultSanitizer.addSanitizerRules(policy, sanitizerRules)
+				defaultSanitizer.rendererPolicies[name] = policy
 			}
 		}
-	}
+		defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
+		defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
+	})
+	return defaultSanitizer
 }
 
-// SanitizeDescription sanitizes the HTML generated for a repository description.
-func SanitizeDescription(s string) string {
-	NewSanitizer()
-	return sanitizer.descriptionPolicy.Sanitize(s)
-}
-
-// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
-func Sanitize(s string) string {
-	NewSanitizer()
-	return sanitizer.defaultPolicy.Sanitize(s)
-}
-
-// SanitizeReader sanitizes a Reader
-func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
-	NewSanitizer()
-	policy, exist := sanitizer.rendererPolicies[renderer]
-	if !exist {
-		policy = sanitizer.defaultPolicy
-	}
-	return policy.SanitizeReaderToWriter(r, w)
+func ResetDefaultSanitizerForTesting() {
+	defaultSanitizer = nil
+	defaultSanitizerOnce = sync.Once{}
 }
diff --git a/modules/markup/sanitizer_custom.go b/modules/markup/sanitizer_custom.go
new file mode 100644
index 0000000000..7978973166
--- /dev/null
+++ b/modules/markup/sanitizer_custom.go
@@ -0,0 +1,25 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/microcosm-cc/bluemonday"
+)
+
+func (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
+	for _, rule := range rules {
+		if rule.AllowDataURIImages {
+			policy.AllowDataURIImages()
+		}
+		if rule.Element != "" {
+			if rule.Regexp != nil {
+				policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
+			} else {
+				policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
+			}
+		}
+	}
+}
diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go
new file mode 100644
index 0000000000..669dc24eae
--- /dev/null
+++ b/modules/markup/sanitizer_default.go
@@ -0,0 +1,146 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"io"
+	"net/url"
+	"regexp"
+
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/microcosm-cc/bluemonday"
+)
+
+func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
+	policy := bluemonday.UGCPolicy()
+
+	// 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("div")
+
+	// 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")
+
+	// For attention
+	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(`^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")
+
+	// For Chroma markdown plugin
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
+
+	// Checkboxes
+	policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
+	policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
+
+	// Custom URL-Schemes
+	if len(setting.Markdown.CustomURLSchemes) > 0 {
+		policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
+	} else {
+		policy.AllowURLSchemesMatching(st.allowAllRegex)
+
+		// Even if every scheme is allowed, these three are blocked for security reasons
+		disallowScheme := func(*url.URL) bool {
+			return false
+		}
+		policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
+		policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
+		policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
+	}
+
+	// Allow classes for anchors
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
+
+	// Allow classes for task lists
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
+
+	// Allow classes for org mode list item status.
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
+
+	// Allow icons
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
+
+	// 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 'color' and 'background-color' properties for the style attribute on text elements.
+	policy.AllowStyles("color", "background-color").OnElements("span", "p")
+
+	// Allow generally safe attributes
+	generalSafeAttrs := []string{
+		"abbr", "accept", "accept-charset",
+		"accesskey", "action", "align", "alt",
+		"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
+		"axis", "border", "cellpadding", "cellspacing", "char",
+		"charoff", "charset", "checked",
+		"clear", "cols", "colspan", "color",
+		"compact", "coords", "datetime", "dir",
+		"disabled", "enctype", "for", "frame",
+		"headers", "height", "hreflang",
+		"hspace", "ismap", "label", "lang",
+		"maxlength", "media", "method",
+		"multiple", "name", "nohref", "noshade",
+		"nowrap", "open", "prompt", "readonly", "rel", "rev",
+		"rows", "rowspan", "rules", "scope",
+		"selected", "shape", "size", "span",
+		"start", "summary", "tabindex", "target",
+		"title", "type", "usemap", "valign", "value",
+		"vspace", "width", "itemprop",
+	}
+
+	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", "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",
+	}
+
+	policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
+
+	policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
+
+	policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
+
+	// FIXME: Need to handle longdesc in img but there is no easy way to do it
+
+	// Custom keyword markup
+	defaultSanitizer.addSanitizerRules(policy, setting.ExternalSanitizerRules)
+
+	return policy
+}
+
+// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
+func Sanitize(s string) string {
+	return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
+}
+
+// SanitizeReader sanitizes a Reader
+func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
+	policy, exist := GetDefaultSanitizer().rendererPolicies[renderer]
+	if !exist {
+		policy = GetDefaultSanitizer().defaultPolicy
+	}
+	return policy.SanitizeReaderToWriter(r, w)
+}
diff --git a/modules/markup/sanitizer_test.go b/modules/markup/sanitizer_default_test.go
similarity index 72%
rename from modules/markup/sanitizer_test.go
rename to modules/markup/sanitizer_default_test.go
index b7b8792bd7..20370509c1 100644
--- a/modules/markup/sanitizer_test.go
+++ b/modules/markup/sanitizer_default_test.go
@@ -5,18 +5,16 @@
 package markup
 
 import (
-	"html/template"
-	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 )
 
-func Test_Sanitizer(t *testing.T) {
-	NewSanitizer()
+func TestSanitizer(t *testing.T) {
 	testCases := []string{
 		// Regular
 		`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
+		"<scrİpt>&lt;script&gt;alert(document.domain)&lt;/script&gt;</scrİpt>", "&lt;script&gt;alert(document.domain)&lt;/script&gt;",
 
 		// Code highlighting class
 		`<code class="random string"></code>`, `<code></code>`,
@@ -72,34 +70,3 @@ func Test_Sanitizer(t *testing.T) {
 		assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
 	}
 }
-
-func TestDescriptionSanitizer(t *testing.T) {
-	NewSanitizer()
-
-	testCases := []string{
-		`<h1>Title</h1>`, `Title`,
-		`<img src='img.png' alt='image'>`, ``,
-		`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
-		`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
-		`<br>`, ``,
-		`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`,
-		`<mark>Important!</mark>`, `Important!`,
-		`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
-		`<input type="hidden">`, ``,
-		`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
-		`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
-	}
-
-	for i := 0; i < len(testCases); i += 2 {
-		assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
-	}
-}
-
-func TestSanitizeNonEscape(t *testing.T) {
-	descStr := "<scrİpt>&lt;script&gt;alert(document.domain)&lt;/script&gt;</scrİpt>"
-
-	output := template.HTML(Sanitize(descStr))
-	if strings.Contains(string(output), "<script>") {
-		t.Errorf("un-escaped <script> in output: %q", output)
-	}
-}
diff --git a/modules/markup/sanitizer_description.go b/modules/markup/sanitizer_description.go
new file mode 100644
index 0000000000..f8b51f2d9a
--- /dev/null
+++ b/modules/markup/sanitizer_description.go
@@ -0,0 +1,37 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"regexp"
+
+	"github.com/microcosm-cc/bluemonday"
+)
+
+// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
+// repository descriptions.
+func (st *Sanitizer) createRepoDescriptionPolicy() *bluemonday.Policy {
+	policy := bluemonday.NewPolicy()
+	policy.AllowStandardURLs()
+
+	// Allow italics and bold.
+	policy.AllowElements("i", "b", "em", "strong")
+
+	// Allow code.
+	policy.AllowElements("code")
+
+	// Allow links
+	policy.AllowAttrs("href", "target", "rel").OnElements("a")
+
+	// Allow classes for emojis
+	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
+	policy.AllowAttrs("aria-label").OnElements("span")
+
+	return policy
+}
+
+// SanitizeDescription sanitizes the HTML generated for a repository description.
+func SanitizeDescription(s string) string {
+	return GetDefaultSanitizer().descriptionPolicy.Sanitize(s)
+}
diff --git a/modules/markup/sanitizer_description_test.go b/modules/markup/sanitizer_description_test.go
new file mode 100644
index 0000000000..ca72491f26
--- /dev/null
+++ b/modules/markup/sanitizer_description_test.go
@@ -0,0 +1,31 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDescriptionSanitizer(t *testing.T) {
+	testCases := []string{
+		`<h1>Title</h1>`, `Title`,
+		`<img src='img.png' alt='image'>`, ``,
+		`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
+		`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
+		`<br>`, ``,
+		`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">https://example.com</a>`,
+		`<a href="data:1234">data</a>`, `data`,
+		`<mark>Important!</mark>`, `Important!`,
+		`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
+		`<input type="hidden">`, ``,
+		`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
+		`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
+	}
+
+	for i := 0; i < len(testCases); i += 2 {
+		assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
+	}
+}

From ab458ce10be59669c810ba43af41f2ba2e72ea5b Mon Sep 17 00:00:00 2001
From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
Date: Sat, 1 Jun 2024 04:49:42 -0700
Subject: [PATCH 076/131] Return an empty string when a repo has no avatar in
 the repo API (#31187)

Resolves #31167.

https://github.com/go-gitea/gitea/pull/30885 changed the behavior of
`repo.AvatarLink()` where it can now take the empty string and append it
to the app data URL. This does not point to a valid avatar image URL,
and, as the issue mentions, previous Gitea versions returned the empty
string.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/repo/avatar.go | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/models/repo/avatar.go b/models/repo/avatar.go
index 8395b8c2b7..ccfac12cad 100644
--- a/models/repo/avatar.go
+++ b/models/repo/avatar.go
@@ -84,7 +84,13 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
 	return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
 }
 
-// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment
+// AvatarLink returns the full avatar url with http host or the empty string if the repo doesn't have an avatar.
+//
+// TODO: refactor it to a relative URL, but it is still used in API response at the moment
 func (repo *Repository) AvatarLink(ctx context.Context) string {
-	return httplib.MakeAbsoluteURL(ctx, repo.relAvatarLink(ctx))
+	relLink := repo.relAvatarLink(ctx)
+	if relLink != "" {
+		return httplib.MakeAbsoluteURL(ctx, relLink)
+	}
+	return ""
 }

From 3cc7f763c3c22ae4c3b5331f8b72b7009c5b11ea Mon Sep 17 00:00:00 2001
From: Max Wipfli <mail@maxwipfli.ch>
Date: Sun, 2 Jun 2024 04:32:20 +0200
Subject: [PATCH 077/131] Only update poster in issue/comment list if it has
 been loaded (#31216)

Previously, all posters were updated, even if they were not part of
posterMaps. In that case, a ghost user was erroneously inserted.

Fixes #31213.
---
 models/issues/comment_list.go | 4 +++-
 models/issues/issue_list.go   | 4 +++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go
index 6b4ad80eed..61ac1c8f56 100644
--- a/models/issues/comment_list.go
+++ b/models/issues/comment_list.go
@@ -32,7 +32,9 @@ func (comments CommentList) LoadPosters(ctx context.Context) error {
 	}
 
 	for _, comment := range comments {
-		comment.Poster = getPoster(comment.PosterID, posterMaps)
+		if comment.Poster == nil {
+			comment.Poster = getPoster(comment.PosterID, posterMaps)
+		}
 	}
 	return nil
 }
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
index 0dd37a04df..2c007c72ec 100644
--- a/models/issues/issue_list.go
+++ b/models/issues/issue_list.go
@@ -87,7 +87,9 @@ func (issues IssueList) LoadPosters(ctx context.Context) error {
 	}
 
 	for _, issue := range issues {
-		issue.Poster = getPoster(issue.PosterID, posterMaps)
+		if issue.Poster == nil {
+			issue.Poster = getPoster(issue.PosterID, posterMaps)
+		}
 	}
 	return nil
 }

From 98a61040b1c83790b0e0e977188842f967ae357e Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sun, 2 Jun 2024 11:01:08 +0800
Subject: [PATCH 078/131] Fix the possible migration failure on 286 with
 postgres 16 (#31209)

Try to fix #31205
---
 models/migrations/v1_22/v286.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go
index e11d16f8de..6ad669f27c 100644
--- a/models/migrations/v1_22/v286.go
+++ b/models/migrations/v1_22/v286.go
@@ -92,7 +92,7 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
 
 	// Here to catch weird edge-cases where column constraints above are
 	// not applied by the DB backend
-	_, err := x.Exec("UPDATE repository set object_format_name = 'sha1' WHERE object_format_name = '' or object_format_name IS NULL")
+	_, err := x.Exec("UPDATE `repository` set `object_format_name` = 'sha1' WHERE `object_format_name` = '' or `object_format_name` IS NULL")
 	return err
 }
 

From 2788a7ca270c8ac2927af021910fad4e1a7b2c7b Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 3 Jun 2024 06:45:21 +0800
Subject: [PATCH 079/131] Fix agit checkout command line hint & fix
 ShowMergeInstructions checking (#31219)

---
 routers/web/repo/issue.go                         | 15 ++++++++-------
 .../view_content/pull_merge_instruction.tmpl      |  4 ++--
 2 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 18f975b4a6..e7ad02c0c2 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1794,6 +1794,7 @@ func ViewIssue(ctx *context.Context) {
 		pull.Issue = issue
 		canDelete := false
 		allowMerge := false
+		canWriteToHeadRepo := false
 
 		if ctx.IsSigned {
 			if err := pull.LoadHeadRepo(ctx); err != nil {
@@ -1814,7 +1815,7 @@ func ViewIssue(ctx *context.Context) {
 							ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
 						}
 					}
-					ctx.Data["CanWriteToHeadRepo"] = true
+					canWriteToHeadRepo = true
 				}
 			}
 
@@ -1826,6 +1827,9 @@ func ViewIssue(ctx *context.Context) {
 				ctx.ServerError("GetUserRepoPermission", err)
 				return
 			}
+			if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it
+				canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode)
+			}
 			allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
 			if err != nil {
 				ctx.ServerError("IsUserAllowedToMerge", err)
@@ -1838,6 +1842,8 @@ func ViewIssue(ctx *context.Context) {
 			}
 		}
 
+		ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
+		ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
 		ctx.Data["AllowMerge"] = allowMerge
 
 		prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
@@ -1892,13 +1898,9 @@ func ViewIssue(ctx *context.Context) {
 			ctx.ServerError("LoadProtectedBranch", err)
 			return
 		}
-		ctx.Data["ShowMergeInstructions"] = true
+
 		if pb != nil {
 			pb.Repo = pull.BaseRepo
-			var showMergeInstructions bool
-			if ctx.Doer != nil {
-				showMergeInstructions = pb.CanUserPush(ctx, ctx.Doer)
-			}
 			ctx.Data["ProtectedBranch"] = pb
 			ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
 			ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
@@ -1909,7 +1911,6 @@ func ViewIssue(ctx *context.Context) {
 			ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
 			ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
 			ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
-			ctx.Data["ShowMergeInstructions"] = showMergeInstructions
 		}
 		ctx.Data["WillSign"] = false
 		if ctx.Doer != nil {
diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
index d585d36574..bb59b49719 100644
--- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl
+++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl
@@ -9,10 +9,10 @@
 	<div class="ui secondary segment">
 		{{if eq .PullRequest.Flow 0}}
 		<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>
+		<div>git fetch -u origin {{.PullRequest.GetGitRefName}}:{{$localBranch}}</div>
 		{{end}}
+		<div>git checkout {{$localBranch}}</div>
 	</div>
 	{{if .ShowMergeInstructions}}
 	<div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_desc"}}</div>

From 9b05bfb173795ba2a2267402d2669715cd4a64e4 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 3 Jun 2024 02:09:51 +0200
Subject: [PATCH 080/131] Fix overflow in issue card (#31203)

Before:

<img width="373" alt="Screenshot 2024-06-01 at 01 31 26"
src="https://github.com/go-gitea/gitea/assets/115237/82a210f2-c82e-4b7e-ac43-e70e46fa1186">

After:
<img width="376" alt="Screenshot 2024-06-01 at 01 31 32"
src="https://github.com/go-gitea/gitea/assets/115237/82d1b9f7-4fad-47bd-948a-04e1e7e006e6">
---
 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 526f6dd5db..4c22c28329 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -14,7 +14,7 @@
 			<div class="issue-card-icon">
 				{{template "shared/issueicon" .}}
 			</div>
-			<a class="issue-card-title muted issue-title" href="{{.Link}}">{{.Title | RenderEmoji ctx | RenderCodeBlock}}</a>
+			<a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{.Title | RenderEmoji ctx | RenderCodeBlock}}</a>
 			{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
 				<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}}

From c6854202be9eb8358f046871d4eb0e4fd74639ff Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 3 Jun 2024 00:27:17 +0000
Subject: [PATCH 081/131] [skip ci] Updated licenses and gitignores

---
 options/gitignore/Alteryx      | 44 ++++++++++++++++++++++++++++++++++
 options/gitignore/Archives     |  2 ++
 options/gitignore/Ballerina    | 11 +++++++++
 options/gitignore/CMake        |  1 +
 options/gitignore/Delphi       | 12 ++++++++++
 options/gitignore/GitHubPages  | 18 ++++++++++++++
 options/gitignore/Go           |  3 +++
 options/gitignore/Objective-C  | 17 -------------
 options/gitignore/Rust         |  7 ++++++
 options/gitignore/Swift        | 28 ----------------------
 options/gitignore/TeX          |  5 ++++
 options/gitignore/Terraform    |  6 +++++
 options/gitignore/UiPath       | 11 +++++++++
 options/gitignore/UnrealEngine |  4 ++--
 options/gitignore/Xcode        |  4 ----
 15 files changed, 122 insertions(+), 51 deletions(-)
 create mode 100644 options/gitignore/Alteryx
 create mode 100644 options/gitignore/Ballerina
 create mode 100644 options/gitignore/GitHubPages
 create mode 100644 options/gitignore/UiPath

diff --git a/options/gitignore/Alteryx b/options/gitignore/Alteryx
new file mode 100644
index 0000000000..a8e1341ffe
--- /dev/null
+++ b/options/gitignore/Alteryx
@@ -0,0 +1,44 @@
+# gitignore template for Alteryx Designer
+# website: https://www.alteryx.com/
+# website: https://help.alteryx.com/current/designer/alteryx-file-types
+
+# Alteryx Data Files
+*.yxdb
+*.cydb
+*.cyidx
+*.rptx
+*.vvf
+*.aws
+
+# Alteryx Special Files
+*.yxwv
+*.yxft
+*.yxbe
+*.bak
+*.pcxml
+*.log
+*.bin
+*.yxlang
+CASS.ini
+
+# Alteryx License Files
+*.yxlc
+*.slc
+*.cylc
+*.alc
+*.gzlc
+
+## gitignore reference sites
+# https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#Ignoring-Files
+# https://git-scm.com/docs/gitignore
+# https://help.github.com/articles/ignoring-files/
+
+## Useful knowledge from stackoverflow
+# Even if you haven't tracked the files so far, git seems to be able to "know" about them even after you add them to .gitignore.
+# WARNING: First commit your current changes, or you will lose them.
+# Then run the following commands from the top folder of your git repo:
+# git rm -r --cached .
+# git add .
+# git commit -m "fixed untracked files"
+
+# author: Kacper Ksieski
\ No newline at end of file
diff --git a/options/gitignore/Archives b/options/gitignore/Archives
index 4ed9ab8350..8c92521b4c 100644
--- a/options/gitignore/Archives
+++ b/options/gitignore/Archives
@@ -14,6 +14,8 @@
 *.lzma
 *.cab
 *.xar
+*.zst
+*.tzst
 
 # Packing-only formats
 *.iso
diff --git a/options/gitignore/Ballerina b/options/gitignore/Ballerina
new file mode 100644
index 0000000000..030a350fbf
--- /dev/null
+++ b/options/gitignore/Ballerina
@@ -0,0 +1,11 @@
+# generated files
+target/
+generated/
+
+# dependencies
+Dependencies.toml
+
+# config files
+Config.toml
+# the config files used for testing, Uncomment the following line if you want to commit the test config files
+#!**/tests/Config.toml
diff --git a/options/gitignore/CMake b/options/gitignore/CMake
index 46f42f8f3c..11c76431e1 100644
--- a/options/gitignore/CMake
+++ b/options/gitignore/CMake
@@ -9,3 +9,4 @@ install_manifest.txt
 compile_commands.json
 CTestTestfile.cmake
 _deps
+CMakeUserPresets.json
diff --git a/options/gitignore/Delphi b/options/gitignore/Delphi
index 9532800ba2..8df99b676b 100644
--- a/options/gitignore/Delphi
+++ b/options/gitignore/Delphi
@@ -26,6 +26,18 @@
 #*.obj
 #
 
+# Default Delphi compiler directories
+# Content of this directories are generated with each Compile/Construct of a project.
+# Most of the time, files here have not there place in a code repository.
+#Win32/
+#Win64/
+#OSX64/
+#OSXARM64/
+#Android/
+#Android64/
+#iOSDevice64/
+#Linux64/
+
 # Delphi compiler-generated binaries (safe to delete)
 *.exe
 *.dll
diff --git a/options/gitignore/GitHubPages b/options/gitignore/GitHubPages
new file mode 100644
index 0000000000..493e69ba39
--- /dev/null
+++ b/options/gitignore/GitHubPages
@@ -0,0 +1,18 @@
+# This .gitignore is appropriate for repositories deployed to GitHub Pages and using
+# a Gemfile as specified at https://github.com/github/pages-gem#conventional
+
+# Basic Jekyll gitignores (synchronize to Jekyll.gitignore)
+_site/
+.sass-cache/
+.jekyll-cache/
+.jekyll-metadata
+
+# Additional Ruby/bundler ignore for when you run: bundle install
+/vendor
+
+# Specific ignore for GitHub Pages
+# GitHub Pages will always use its own deployed version of pages-gem 
+# This means GitHub Pages will NOT use your Gemfile.lock and therefore it is
+# counterproductive to check this file into the repository.
+# Details at https://github.com/github/pages-gem/issues/768
+Gemfile.lock
diff --git a/options/gitignore/Go b/options/gitignore/Go
index 6f6f5e6adc..6f72f89261 100644
--- a/options/gitignore/Go
+++ b/options/gitignore/Go
@@ -20,3 +20,6 @@
 # Go workspace file
 go.work
 go.work.sum
+
+# env file
+.env
diff --git a/options/gitignore/Objective-C b/options/gitignore/Objective-C
index 7801c93000..9b8cd0706f 100644
--- a/options/gitignore/Objective-C
+++ b/options/gitignore/Objective-C
@@ -5,23 +5,6 @@
 ## User settings
 xcuserdata/
 
-## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
-*.xcscmblueprint
-*.xccheckout
-
-## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
-build/
-DerivedData/
-*.moved-aside
-*.pbxuser
-!default.pbxuser
-*.mode1v3
-!default.mode1v3
-*.mode2v3
-!default.mode2v3
-*.perspectivev3
-!default.perspectivev3
-
 ## Obj-C/Swift specific
 *.hmap
 
diff --git a/options/gitignore/Rust b/options/gitignore/Rust
index 6985cf1bd0..d01bd1a990 100644
--- a/options/gitignore/Rust
+++ b/options/gitignore/Rust
@@ -12,3 +12,10 @@ Cargo.lock
 
 # MSVC Windows builds of rustc generate these, which store debugging information
 *.pdb
+
+# RustRover
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
\ No newline at end of file
diff --git a/options/gitignore/Swift b/options/gitignore/Swift
index 330d1674f3..52fe2f7102 100644
--- a/options/gitignore/Swift
+++ b/options/gitignore/Swift
@@ -5,23 +5,6 @@
 ## User settings
 xcuserdata/
 
-## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
-*.xcscmblueprint
-*.xccheckout
-
-## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
-build/
-DerivedData/
-*.moved-aside
-*.pbxuser
-!default.pbxuser
-*.mode1v3
-!default.mode1v3
-*.mode2v3
-!default.mode2v3
-*.perspectivev3
-!default.perspectivev3
-
 ## Obj-C/Swift specific
 *.hmap
 
@@ -66,10 +49,6 @@ playground.xcworkspace
 
 Carthage/Build/
 
-# Accio dependency management
-Dependencies/
-.accio/
-
 # fastlane
 #
 # It is recommended to not store the screenshots in the git repo.
@@ -81,10 +60,3 @@ fastlane/report.xml
 fastlane/Preview.html
 fastlane/screenshots/**/*.png
 fastlane/test_output
-
-# Code Injection
-#
-# After new code Injection tools there's a generated folder /iOSInjectionProject
-# https://github.com/johnno1962/injectionforxcode
-
-iOSInjectionProject/
diff --git a/options/gitignore/TeX b/options/gitignore/TeX
index e964244133..a1f5212090 100644
--- a/options/gitignore/TeX
+++ b/options/gitignore/TeX
@@ -39,6 +39,8 @@
 *.synctex.gz
 *.synctex.gz(busy)
 *.pdfsync
+*.rubbercache
+rubber.cache
 
 ## Build tool directories for auxiliary files
 # latexrun
@@ -138,6 +140,9 @@ acs-*.bib
 *.trc
 *.xref
 
+# hypdoc
+*.hd
+
 # hyperref
 *.brf
 
diff --git a/options/gitignore/Terraform b/options/gitignore/Terraform
index 9b8a46e692..15073ca88b 100644
--- a/options/gitignore/Terraform
+++ b/options/gitignore/Terraform
@@ -23,6 +23,9 @@ override.tf.json
 *_override.tf
 *_override.tf.json
 
+# Ignore transient lock info files created by terraform apply
+.terraform.tfstate.lock.info
+
 # Include override files you do wish to add to version control using negated pattern
 # !example_override.tf
 
@@ -32,3 +35,6 @@ override.tf.json
 # Ignore CLI configuration files
 .terraformrc
 terraform.rc
+
+# Ignore hcl file
+.terraform.lock.hcl
diff --git a/options/gitignore/UiPath b/options/gitignore/UiPath
new file mode 100644
index 0000000000..f0c2267b89
--- /dev/null
+++ b/options/gitignore/UiPath
@@ -0,0 +1,11 @@
+# gitignore template for RPA development using UiPath Studio 
+# website: https://www.uipath.com/product/studio
+#
+# Recommended: n/a
+
+# Ignore folders that could cause issues if accidentally tracked
+**/.local/**
+**/.settings/**
+**/.objects/**
+**/.tmh/**
+**/*.log
diff --git a/options/gitignore/UnrealEngine b/options/gitignore/UnrealEngine
index 6582eaf9a1..6e0d95fb31 100644
--- a/options/gitignore/UnrealEngine
+++ b/options/gitignore/UnrealEngine
@@ -47,7 +47,7 @@ SourceArt/**/*.tga
 
 # Binary Files
 Binaries/*
-Plugins/*/Binaries/*
+Plugins/**/Binaries/*
 
 # Builds
 Build/*
@@ -68,7 +68,7 @@ Saved/*
 
 # Compiled source files for the engine to use
 Intermediate/*
-Plugins/*/Intermediate/*
+Plugins/**/Intermediate/*
 
 # Cache files for the editor to use
 DerivedDataCache/*
diff --git a/options/gitignore/Xcode b/options/gitignore/Xcode
index f87d2f2e74..5073505e08 100644
--- a/options/gitignore/Xcode
+++ b/options/gitignore/Xcode
@@ -1,6 +1,2 @@
 ## User settings
 xcuserdata/
-
-## Xcode 8 and earlier
-*.xcscmblueprint
-*.xccheckout

From 4b20b51f8260cb012581493d1143033dd3936aa6 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 3 Jun 2024 09:04:35 +0200
Subject: [PATCH 082/131] Update golangci-lint to v1.59.0 (#31221)

One new error regarding `fmt.Fscanf` error return in `gitdiff.go` but
I'm not touching that further right now as handling the error would
introduce a behaviour difference.
---
 Makefile                    | 2 +-
 services/gitdiff/gitdiff.go | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/Makefile b/Makefile
index 80efcbe46d..f273cac3a8 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ XGO_VERSION := go-1.22.x
 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
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.0
 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 063c995d52..0ddd5a48e2 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -1061,7 +1061,7 @@ func readFileName(rd *strings.Reader) (string, bool) {
 	char, _ := rd.ReadByte()
 	_ = rd.UnreadByte()
 	if char == '"' {
-		fmt.Fscanf(rd, "%q ", &name)
+		_, _ = fmt.Fscanf(rd, "%q ", &name)
 		if len(name) == 0 {
 			log.Error("Reader has no file name: reader=%+v", rd)
 			return "", true
@@ -1073,12 +1073,12 @@ func readFileName(rd *strings.Reader) (string, bool) {
 	} else {
 		// This technique is potentially ambiguous it may not be possible to uniquely identify the filenames from the diff line alone
 		ambiguity = true
-		fmt.Fscanf(rd, "%s ", &name)
+		_, _ = fmt.Fscanf(rd, "%s ", &name)
 		char, _ := rd.ReadByte()
 		_ = rd.UnreadByte()
 		for !(char == 0 || char == '"' || char == 'b') {
 			var suffix string
-			fmt.Fscanf(rd, "%s ", &suffix)
+			_, _ = fmt.Fscanf(rd, "%s ", &suffix)
 			name += " " + suffix
 			char, _ = rd.ReadByte()
 			_ = rd.UnreadByte()

From fc641b3a28300e13c822140556eca8d00f2b5196 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Mon, 3 Jun 2024 19:41:29 +0900
Subject: [PATCH 083/131] Remove sqlite-viewer and using database client
 (#31223)

sqlite-viewer can not edit sqlite.
database client can connect to almost all common databases, which is
very useful I think. Of cause, it can edit sqlite.

https://marketplace.visualstudio.com/items?itemName=cweijan.vscode-database-client2

And for using sqlite, sqlite3 is required. So also added a new feature:
https://github.com/warrenbuckley/codespace-features
found from: https://containers.dev/features
---
 .devcontainer/devcontainer.json | 5 +++--
 .gitpod.yml                     | 2 +-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index c32c5da82c..1b0255d198 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -10,7 +10,8 @@
     "ghcr.io/devcontainers-contrib/features/poetry:2": {},
     "ghcr.io/devcontainers/features/python:1": {
       "version": "3.12"
-    }
+    },
+    "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
   },
   "customizations": {
     "vscode": {
@@ -25,7 +26,7 @@
         "Vue.volar",
         "ms-azuretools.vscode-docker",
         "vitest.explorer",
-        "qwtel.sqlite-viewer",
+        "cweijan.vscode-database-client2",
         "GitHub.vscode-pull-request-github",
         "Azurite.azurite"
       ]
diff --git a/.gitpod.yml b/.gitpod.yml
index f573d55a76..8671edc47c 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -43,7 +43,7 @@ vscode:
     - Vue.volar
     - ms-azuretools.vscode-docker
     - vitest.explorer
-    - qwtel.sqlite-viewer
+    - cweijan.vscode-database-client2
     - GitHub.vscode-pull-request-github
 
 ports:

From cb27c438a82fec9f2476f6058bc5dcda2617aab5 Mon Sep 17 00:00:00 2001
From: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
Date: Mon, 3 Jun 2024 06:40:48 -0700
Subject: [PATCH 084/131] Document possible action types for the user activity
 feed API (#31196)

Resolves #31131.

It uses the the go-swagger `enum` property to document the activity
action types.
---
 modules/structs/activity.go    |  7 +++++--
 templates/swagger/v1_json.tmpl | 30 ++++++++++++++++++++++++++++++
 2 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/modules/structs/activity.go b/modules/structs/activity.go
index 6d2ee56b08..ea27fbfd77 100644
--- a/modules/structs/activity.go
+++ b/modules/structs/activity.go
@@ -6,8 +6,11 @@ package structs
 import "time"
 
 type Activity struct {
-	ID        int64       `json:"id"`
-	UserID    int64       `json:"user_id"` // Receiver user
+	ID     int64 `json:"id"`
+	UserID int64 `json:"user_id"` // Receiver user
+	// the type of action
+	//
+	// enum: create_repo,rename_repo,star_repo,watch_repo,commit_repo,create_issue,create_pull_request,transfer_repo,push_tag,comment_issue,merge_pull_request,close_issue,reopen_issue,close_pull_request,reopen_pull_request,delete_tag,delete_branch,mirror_sync_push,mirror_sync_create,mirror_sync_delete,approve_pull_request,reject_pull_request,comment_pull,publish_release,pull_review_dismissed,pull_request_ready_for_review,auto_merge_pull_request
 	OpType    string      `json:"op_type"`
 	ActUserID int64       `json:"act_user_id"`
 	ActUser   *User       `json:"act_user"`
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index c552e48346..34f09f0587 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -18178,7 +18178,37 @@
           "x-go-name": "IsPrivate"
         },
         "op_type": {
+          "description": "the type of action",
           "type": "string",
+          "enum": [
+            "create_repo",
+            "rename_repo",
+            "star_repo",
+            "watch_repo",
+            "commit_repo",
+            "create_issue",
+            "create_pull_request",
+            "transfer_repo",
+            "push_tag",
+            "comment_issue",
+            "merge_pull_request",
+            "close_issue",
+            "reopen_issue",
+            "close_pull_request",
+            "reopen_pull_request",
+            "delete_tag",
+            "delete_branch",
+            "mirror_sync_push",
+            "mirror_sync_create",
+            "mirror_sync_delete",
+            "approve_pull_request",
+            "reject_pull_request",
+            "comment_pull",
+            "publish_release",
+            "pull_review_dismissed",
+            "pull_request_ready_for_review",
+            "auto_merge_pull_request"
+          ],
           "x-go-name": "OpType"
         },
         "ref_name": {

From 0f0db6a14fd10a493ba73f211e2e627c3884d114 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 3 Jun 2024 19:21:45 +0200
Subject: [PATCH 085/131] Remove unnecessary inline style for tab-size (#31224)

Move the rule to the parent node. `tab-size` is inherited so will work
just as before.
---
 routers/web/repo/issue_content_history.go | 2 +-
 web_src/css/repo.css                      | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index bf3571c835..a7362113e3 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -156,7 +156,7 @@ func GetContentHistoryDetail(ctx *context.Context) {
 
 	// use chroma to render the diff html
 	diffHTMLBuf := bytes.Buffer{}
-	diffHTMLBuf.WriteString("<pre class='chroma' style='tab-size: 4'>")
+	diffHTMLBuf.WriteString("<pre class='chroma'>")
 	for _, it := range diff {
 		if it.Type == diffmatchpatch.DiffInsert {
 			diffHTMLBuf.WriteString("<span class='gi'>")
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index d3036744fe..e44bc9811b 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2322,6 +2322,7 @@ tbody.commit-list {
   min-height: 12em;
   max-height: calc(100vh - 10.5rem);
   overflow-y: auto;
+  tab-size: 4;
 }
 
 .comment-diff-data pre {

From 8c68c5e436805848197d98313e9ee77e8d540a83 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 3 Jun 2024 20:21:28 +0200
Subject: [PATCH 086/131] Move custom `tw-` helpers to tailwind plugin (#31184)

Move the previous custom `tw-` classes to be defined in a tailwind
plugin. I think it's cleaner that way and I also verified double-class
works as expected:

<img width="299" alt="Screenshot 2024-05-30 at 19 06 24"
src="https://github.com/go-gitea/gitea/assets/115237/003cbc76-2013-46a0-9e27-63023fa7c7a4">
---
 tailwind.config.js      | 23 +++++++++++++++++++++++
 web_src/css/helpers.css | 16 ----------------
 2 files changed, 23 insertions(+), 16 deletions(-)

diff --git a/tailwind.config.js b/tailwind.config.js
index 94dfdbced4..8f3e8c8251 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,6 +1,7 @@
 import {readFileSync} from 'node:fs';
 import {env} from 'node:process';
 import {parse} from 'postcss';
+import plugin from 'tailwindcss/plugin.js';
 
 const isProduction = env.NODE_ENV !== 'development';
 
@@ -98,4 +99,26 @@ export default {
       })),
     },
   },
+  plugins: [
+    plugin(({addUtilities}) => {
+      addUtilities({
+        // 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 ".tw-hidden" class
+        // * showElem/hideElem/toggleElem functions in "utils/dom.js"
+        '.hidden.hidden': {
+          'display': 'none',
+        },
+        // proposed class from https://github.com/tailwindlabs/tailwindcss/pull/12128
+        '.break-anywhere': {
+          'overflow-wrap': 'anywhere',
+        },
+      });
+    }),
+  ],
 };
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 60ecd7db72..15df9f3a45 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -35,22 +35,6 @@ Gitea's private styles use `g-` prefix.
 .interact-bg:hover { background: var(--color-hover) !important; }
 .interact-bg:active { background: var(--color-active) !important; }
 
-/*
-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 ".tw-hidden" class
-* showElem/hideElem/toggleElem functions in "utils/dom.js"
-*/
-.tw-hidden.tw-hidden { display: none !important; }
-
-/* proposed class from https://github.com/tailwindlabs/tailwindcss/pull/12128 */
-.tw-break-anywhere { overflow-wrap: anywhere !important; }
-
 @media (max-width: 767.98px) {
   /* double selector so it wins over .tw-flex (old .gt-df) etc */
   .not-mobile.not-mobile {

From aace3bccc3290446637cac30b121b94b5d03075f Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Mon, 3 Jun 2024 20:42:52 +0200
Subject: [PATCH 087/131] Add option for mailer to override mail headers
 (#27860)

Add option to override headers of mails, gitea send out

---
*Sponsored by Kithara Software GmbH*
---
 custom/conf/app.example.ini                   | 10 +++
 .../config-cheat-sheet.en-us.md               | 19 ++++-
 modules/setting/mailer.go                     | 23 ++++--
 services/mailer/mailer.go                     | 10 ++-
 services/mailer/mailer_test.go                | 76 +++++++++++++++++++
 5 files changed, 128 insertions(+), 10 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index be5d632f54..7677168d83 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1687,6 +1687,16 @@ LEVEL = Info
 ;; convert \r\n to \n for Sendmail
 ;SENDMAIL_CONVERT_CRLF = true
 
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;[mailer.override_header]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; This is empty by default, use it only if you know what you need it for.
+;Reply-To = test@example.com, test2@example.com
+;Content-Type = text/html; charset=utf-8
+;In-Reply-To =
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;[email.incoming]
diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md
index aabf1b20d8..0c15a866b6 100644
--- a/docs/content/administration/config-cheat-sheet.en-us.md
+++ b/docs/content/administration/config-cheat-sheet.en-us.md
@@ -724,11 +724,13 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
 
 ## Mailer (`mailer`)
 
-⚠️ This section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older,
+:::warning
+This section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older,
 please refer to
 [Gitea 1.17 app.ini example](https://github.com/go-gitea/gitea/blob/release/v1.17/custom/conf/app.example.ini)
 and
 [Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md)
+:::
 
 - `ENABLED`: **false**: Enable to use a mail service.
 - `PROTOCOL`: **_empty_**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
@@ -761,6 +763,21 @@ and
 - `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]`
 - `SEND_AS_PLAIN_TEXT`: **false**: Send mails only in plain text, without HTML alternative.
 
+## Override Email Headers (`mailer.override_header`)
+
+:::warning
+This is empty by default, use it only if you know what you need it for.
+:::
+
+examples would be:
+
+```ini
+[mailer.override_header]
+Reply-To = test@example.com, test2@example.com
+Content-Type = text/html; charset=utf-8
+In-Reply-To =
+```
+
 ## Incoming Email (`email.incoming`)
 
 - `ENABLED`: **false**: Enable handling of incoming emails.
diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go
index a2bc2df444..58bfd67bfb 100644
--- a/modules/setting/mailer.go
+++ b/modules/setting/mailer.go
@@ -18,14 +18,15 @@ import (
 // Mailer represents mail service.
 type Mailer struct {
 	// Mailer
-	Name                 string `ini:"NAME"`
-	From                 string `ini:"FROM"`
-	EnvelopeFrom         string `ini:"ENVELOPE_FROM"`
-	OverrideEnvelopeFrom bool   `ini:"-"`
-	FromName             string `ini:"-"`
-	FromEmail            string `ini:"-"`
-	SendAsPlainText      bool   `ini:"SEND_AS_PLAIN_TEXT"`
-	SubjectPrefix        string `ini:"SUBJECT_PREFIX"`
+	Name                 string              `ini:"NAME"`
+	From                 string              `ini:"FROM"`
+	EnvelopeFrom         string              `ini:"ENVELOPE_FROM"`
+	OverrideEnvelopeFrom bool                `ini:"-"`
+	FromName             string              `ini:"-"`
+	FromEmail            string              `ini:"-"`
+	SendAsPlainText      bool                `ini:"SEND_AS_PLAIN_TEXT"`
+	SubjectPrefix        string              `ini:"SUBJECT_PREFIX"`
+	OverrideHeader       map[string][]string `ini:"-"`
 
 	// SMTP sender
 	Protocol             string `ini:"PROTOCOL"`
@@ -151,6 +152,12 @@ func loadMailerFrom(rootCfg ConfigProvider) {
 		log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err)
 	}
 
+	overrideHeader := rootCfg.Section("mailer.override_header").Keys()
+	MailService.OverrideHeader = make(map[string][]string)
+	for _, key := range overrideHeader {
+		MailService.OverrideHeader[key.Name()] = key.Strings(",")
+	}
+
 	// Infer SMTPPort if not set
 	if MailService.SMTPPort == "" {
 		switch MailService.Protocol {
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index 5e8e3dbb38..c5846e6104 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -57,7 +57,7 @@ func (m *Message) ToMessage() *gomail.Message {
 		msg.SetHeader(header, m.Headers[header]...)
 	}
 
-	if len(setting.MailService.SubjectPrefix) > 0 {
+	if setting.MailService.SubjectPrefix != "" {
 		msg.SetHeader("Subject", setting.MailService.SubjectPrefix+" "+m.Subject)
 	} else {
 		msg.SetHeader("Subject", m.Subject)
@@ -79,6 +79,14 @@ func (m *Message) ToMessage() *gomail.Message {
 	if len(msg.GetHeader("Message-ID")) == 0 {
 		msg.SetHeader("Message-ID", m.generateAutoMessageID())
 	}
+
+	for k, v := range setting.MailService.OverrideHeader {
+		if len(msg.GetHeader(k)) != 0 {
+			log.Debug("Mailer override header '%s' as per config", k)
+		}
+		msg.SetHeader(k, v...)
+	}
+
 	return msg
 }
 
diff --git a/services/mailer/mailer_test.go b/services/mailer/mailer_test.go
index 375ca35daa..6d7c44f40c 100644
--- a/services/mailer/mailer_test.go
+++ b/services/mailer/mailer_test.go
@@ -4,6 +4,7 @@
 package mailer
 
 import (
+	"strings"
 	"testing"
 	"time"
 
@@ -36,3 +37,78 @@ func TestGenerateMessageID(t *testing.T) {
 	gm = m.ToMessage()
 	assert.Equal(t, "<msg-d@domain.com>", gm.GetHeader("Message-ID")[0])
 }
+
+func TestToMessage(t *testing.T) {
+	oldConf := *setting.MailService
+	defer func() {
+		setting.MailService = &oldConf
+	}()
+	setting.MailService.From = "test@gitea.com"
+
+	m1 := Message{
+		Info:            "info",
+		FromAddress:     "test@gitea.com",
+		FromDisplayName: "Test Gitea",
+		To:              "a@b.com",
+		Subject:         "Issue X Closed",
+		Body:            "Some Issue got closed by Y-Man",
+	}
+
+	buf := &strings.Builder{}
+	_, err := m1.ToMessage().WriteTo(buf)
+	assert.NoError(t, err)
+	header, _ := extractMailHeaderAndContent(t, buf.String())
+	assert.EqualValues(t, map[string]string{
+		"Content-Type":             "multipart/alternative;",
+		"Date":                     "Mon, 01 Jan 0001 00:00:00 +0000",
+		"From":                     "\"Test Gitea\" <test@gitea.com>",
+		"Message-ID":               "<autogen--6795364578871-69c000786adc60dc@localhost>",
+		"Mime-Version":             "1.0",
+		"Subject":                  "Issue X Closed",
+		"To":                       "a@b.com",
+		"X-Auto-Response-Suppress": "All",
+	}, header)
+
+	setting.MailService.OverrideHeader = map[string][]string{
+		"Message-ID":     {""},               // delete message id
+		"Auto-Submitted": {"auto-generated"}, // suppress auto replay
+	}
+
+	buf = &strings.Builder{}
+	_, err = m1.ToMessage().WriteTo(buf)
+	assert.NoError(t, err)
+	header, _ = extractMailHeaderAndContent(t, buf.String())
+	assert.EqualValues(t, map[string]string{
+		"Content-Type":             "multipart/alternative;",
+		"Date":                     "Mon, 01 Jan 0001 00:00:00 +0000",
+		"From":                     "\"Test Gitea\" <test@gitea.com>",
+		"Message-ID":               "",
+		"Mime-Version":             "1.0",
+		"Subject":                  "Issue X Closed",
+		"To":                       "a@b.com",
+		"X-Auto-Response-Suppress": "All",
+		"Auto-Submitted":           "auto-generated",
+	}, header)
+}
+
+func extractMailHeaderAndContent(t *testing.T, mail string) (map[string]string, string) {
+	header := make(map[string]string)
+
+	parts := strings.SplitN(mail, "boundary=", 2)
+	if !assert.Len(t, parts, 2) {
+		return nil, ""
+	}
+	content := strings.TrimSpace("boundary=" + parts[1])
+
+	hParts := strings.Split(parts[0], "\n")
+
+	for _, hPart := range hParts {
+		parts := strings.SplitN(hPart, ":", 2)
+		hk := strings.TrimSpace(parts[0])
+		if hk != "" {
+			header[hk] = strings.TrimSpace(parts[1])
+		}
+	}
+
+	return header, content
+}

From 433963e52ccbe2f469c83a0252ea4cab9b34a467 Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Tue, 4 Jun 2024 06:01:06 +0300
Subject: [PATCH 088/131] Bump `@github/relative-time-element` to v4.4.1
 (#31232)

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 90cedd63d5..8b1ba766d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
         "@citation-js/plugin-csl": "0.7.11",
         "@citation-js/plugin-software-formats": "0.6.1",
         "@github/markdown-toolbar-element": "2.2.3",
-        "@github/relative-time-element": "4.4.0",
+        "@github/relative-time-element": "4.4.1",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
         "@primer/octicons": "19.9.0",
@@ -1028,9 +1028,9 @@
       "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
     },
     "node_modules/@github/relative-time-element": {
-      "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=="
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.1.tgz",
+      "integrity": "sha512-E2vRcIgDj8AHv/iHpQMLJ/RqKOJ704OXkKw6+Zdhk3X+kVQhOf3Wj8KVz4DfCQ1eOJR8XxY6XVv73yd+pjMfXA=="
     },
     "node_modules/@github/text-expander-element": {
       "version": "2.6.1",
diff --git a/package.json b/package.json
index d7588e093f..5add488bb6 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
     "@citation-js/plugin-csl": "0.7.11",
     "@citation-js/plugin-software-formats": "0.6.1",
     "@github/markdown-toolbar-element": "2.2.3",
-    "@github/relative-time-element": "4.4.0",
+    "@github/relative-time-element": "4.4.1",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
     "@primer/octicons": "19.9.0",

From 93570de4968b7ea843f669b173c373c6fbd1c64a Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Tue, 4 Jun 2024 14:00:44 +0900
Subject: [PATCH 089/131] Update air package path (#31233)

---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index f273cac3a8..e9dc945206 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ COMMA := ,
 
 XGO_VERSION := go-1.22.x
 
-AIR_PACKAGE ?= github.com/cosmtrek/air@v1
+AIR_PACKAGE ?= github.com/air-verse/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.59.0

From a7557494cad5aa850536e17cdaf93988f7daa56e Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 4 Jun 2024 07:34:34 +0200
Subject: [PATCH 090/131] Update chroma to v2.14.0 (#31177)

https://github.com/alecthomas/chroma/releases/tag/v2.14.0

Tested it with a typescript file.
---
 go.mod | 2 +-
 go.sum | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/go.mod b/go.mod
index 87f2b00e6a..6f739ed6e9 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
 	github.com/ProtonMail/go-crypto v1.0.0
 	github.com/PuerkitoBio/goquery v1.9.1
-	github.com/alecthomas/chroma/v2 v2.13.0
+	github.com/alecthomas/chroma/v2 v2.14.0
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
 	github.com/blevesearch/bleve/v2 v2.3.10
 	github.com/buildkite/terminal-to-html/v3 v3.11.0
diff --git a/go.sum b/go.sum
index 84f7121908..543bd70866 100644
--- a/go.sum
+++ b/go.sum
@@ -82,11 +82,11 @@ 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.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/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
+github.com/alecthomas/assert/v2 v2.7.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.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
-github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=

From 4f9b8b397c1acb6f6d26c55e224aafcb5474a85b Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 4 Jun 2024 08:10:04 +0200
Subject: [PATCH 091/131] Fix overflow on notifications (#31178)

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

<img width="1312" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/627711ed-93ca-4be6-b958-10d673ae9517">
---
 templates/user/notification/notification_div.tmpl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl
index bf3b51ee3b..9790a7087a 100644
--- a/templates/user/notification/notification_div.tmpl
+++ b/templates/user/notification/notification_div.tmpl
@@ -44,14 +44,14 @@
 								{{end}}
 							</div>
 							<a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}">
-								<div class="notifications-top-row tw-text-13">
+								<div class="notifications-top-row tw-text-13 tw-break-anywhere">
 									{{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}}
 									{{if eq .Status 3}}
 										{{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}}
 									{{end}}
 								</div>
 								<div class="notifications-bottom-row tw-text-16 tw-py-0.5">
-									<span class="issue-title">
+									<span class="issue-title tw-break-anywhere">
 										{{if .Issue}}
 											{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}
 										{{else}}

From c888c933a930ee2ba4e7bb0bf6678aaf45a9778a Mon Sep 17 00:00:00 2001
From: Thomas Desveaux <thomas.desveaux@dont-nod.com>
Date: Tue, 4 Jun 2024 08:45:56 +0200
Subject: [PATCH 092/131] Fix NuGet Package API for $filter with Id equality 
 (#31188)

Fixes issue when running `choco info pkgname` where `pkgname` is also a
substring of another package Id.

Relates to #31168

---

This might fix the issue linked, but I'd like to test it with more choco
commands before closing the issue in case I find other problems if
that's ok.

---------

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 routers/api/packages/nuget/nuget.go          |  48 +++++----
 tests/integration/api_packages_nuget_test.go | 102 ++++++++++++++++---
 2 files changed, 115 insertions(+), 35 deletions(-)

diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index 26b0ae226e..3633d0d007 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -96,20 +96,34 @@ func FeedCapabilityResource(ctx *context.Context) {
 	xmlResponse(ctx, http.StatusOK, Metadata)
 }
 
-var searchTermExtract = regexp.MustCompile(`'([^']+)'`)
+var (
+	searchTermExtract = regexp.MustCompile(`'([^']+)'`)
+	searchTermExact   = regexp.MustCompile(`\s+eq\s+'`)
+)
 
-func getSearchTerm(ctx *context.Context) string {
+func getSearchTerm(ctx *context.Context) packages_model.SearchValue {
 	searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
-	if searchTerm == "" {
-		// $filter contains a query like:
-		// (((Id ne null) and substringof('microsoft',tolower(Id)))
-		// We don't support these queries, just extract the search term.
-		match := searchTermExtract.FindStringSubmatch(ctx.FormTrim("$filter"))
-		if len(match) == 2 {
-			searchTerm = strings.TrimSpace(match[1])
+	if searchTerm != "" {
+		return packages_model.SearchValue{
+			Value:      searchTerm,
+			ExactMatch: false,
 		}
 	}
-	return searchTerm
+
+	// $filter contains a query like:
+	// (((Id ne null) and substringof('microsoft',tolower(Id)))
+	// https://www.odata.org/documentation/odata-version-2-0/uri-conventions/ section 4.5
+	// We don't support these queries, just extract the search term.
+	filter := ctx.FormTrim("$filter")
+	match := searchTermExtract.FindStringSubmatch(filter)
+	if len(match) == 2 {
+		return packages_model.SearchValue{
+			Value:      strings.TrimSpace(match[1]),
+			ExactMatch: searchTermExact.MatchString(filter),
+		}
+	}
+
+	return packages_model.SearchValue{}
 }
 
 // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
@@ -118,11 +132,9 @@ func SearchServiceV2(ctx *context.Context) {
 	paginator := db.NewAbsoluteListOptions(skip, take)
 
 	pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
-		OwnerID: ctx.Package.Owner.ID,
-		Type:    packages_model.TypeNuGet,
-		Name: packages_model.SearchValue{
-			Value: getSearchTerm(ctx),
-		},
+		OwnerID:    ctx.Package.Owner.ID,
+		Type:       packages_model.TypeNuGet,
+		Name:       getSearchTerm(ctx),
 		IsInternal: optional.Some(false),
 		Paginator:  paginator,
 	})
@@ -169,10 +181,8 @@ func SearchServiceV2(ctx *context.Context) {
 // http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
 func SearchServiceV2Count(ctx *context.Context) {
 	count, err := nuget_model.CountPackages(ctx, &packages_model.PackageSearchOptions{
-		OwnerID: ctx.Package.Owner.ID,
-		Name: packages_model.SearchValue{
-			Value: getSearchTerm(ctx),
-		},
+		OwnerID:    ctx.Package.Owner.ID,
+		Name:       getSearchTerm(ctx),
 		IsInternal: optional.Some(false),
 	})
 	if err != nil {
diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go
index 83947ff967..630b4de3f9 100644
--- a/tests/integration/api_packages_nuget_test.go
+++ b/tests/integration/api_packages_nuget_test.go
@@ -429,22 +429,33 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
 
 	t.Run("SearchService", func(t *testing.T) {
 		cases := []struct {
-			Query           string
-			Skip            int
-			Take            int
-			ExpectedTotal   int64
-			ExpectedResults int
+			Query              string
+			Skip               int
+			Take               int
+			ExpectedTotal      int64
+			ExpectedResults    int
+			ExpectedExactMatch bool
 		}{
-			{"", 0, 0, 1, 1},
-			{"", 0, 10, 1, 1},
-			{"gitea", 0, 10, 0, 0},
-			{"test", 0, 10, 1, 1},
-			{"test", 1, 10, 1, 0},
+			{"", 0, 0, 4, 4, false},
+			{"", 0, 10, 4, 4, false},
+			{"gitea", 0, 10, 0, 0, false},
+			{"test", 0, 10, 1, 1, false},
+			{"test", 1, 10, 1, 0, false},
+			{"almost.similar", 0, 0, 3, 3, true},
 		}
 
-		req := NewRequestWithBody(t, "PUT", url, createPackage(packageName, "1.0.99")).
-			AddBasicAuth(user.Name)
-		MakeRequest(t, req, http.StatusCreated)
+		fakePackages := []string{
+			packageName,
+			"almost.similar.dependency",
+			"almost.similar",
+			"almost.similar.dependant",
+		}
+
+		for _, fakePackageName := range fakePackages {
+			req := NewRequestWithBody(t, "PUT", url, createPackage(fakePackageName, "1.0.99")).
+				AddBasicAuth(user.Name)
+			MakeRequest(t, req, http.StatusCreated)
+		}
 
 		t.Run("v2", func(t *testing.T) {
 			t.Run("Search()", func(t *testing.T) {
@@ -491,6 +502,63 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
 				}
 			})
 
+			t.Run("Packages()", func(t *testing.T) {
+				defer tests.PrintCurrentTest(t)()
+
+				t.Run("substringof", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					for i, c := range cases {
+						req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
+							AddBasicAuth(user.Name)
+						resp := MakeRequest(t, req, http.StatusOK)
+
+						var result FeedResponse
+						decodeXML(t, resp, &result)
+
+						assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
+						assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
+
+						req = NewRequest(t, "GET", fmt.Sprintf("%s/Packages()/$count?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
+							AddBasicAuth(user.Name)
+						resp = MakeRequest(t, req, http.StatusOK)
+
+						assert.Equal(t, strconv.FormatInt(c.ExpectedTotal, 10), resp.Body.String(), "case %d: unexpected total hits", i)
+					}
+				})
+
+				t.Run("IdEq", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					for i, c := range cases {
+						if c.Query == "" {
+							// Ignore the `tolower(Id) eq ''` as it's unlikely to happen
+							continue
+						}
+						req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages()?$filter=(tolower(Id) eq '%s')&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
+							AddBasicAuth(user.Name)
+						resp := MakeRequest(t, req, http.StatusOK)
+
+						var result FeedResponse
+						decodeXML(t, resp, &result)
+
+						expectedCount := 0
+						if c.ExpectedExactMatch {
+							expectedCount = 1
+						}
+
+						assert.Equal(t, int64(expectedCount), result.Count, "case %d: unexpected total hits", i)
+						assert.Len(t, result.Entries, expectedCount, "case %d: unexpected result count", i)
+
+						req = NewRequest(t, "GET", fmt.Sprintf("%s/Packages()/$count?$filter=(tolower(Id) eq '%s')&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
+							AddBasicAuth(user.Name)
+						resp = MakeRequest(t, req, http.StatusOK)
+
+						assert.Equal(t, strconv.FormatInt(int64(expectedCount), 10), resp.Body.String(), "case %d: unexpected total hits", i)
+					}
+				})
+			})
+
 			t.Run("Next", func(t *testing.T) {
 				req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?searchTerm='test'&$skip=0&$top=1", url)).
 					AddBasicAuth(user.Name)
@@ -548,9 +616,11 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
 			})
 		})
 
-		req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, "1.0.99")).
-			AddBasicAuth(user.Name)
-		MakeRequest(t, req, http.StatusNoContent)
+		for _, fakePackageName := range fakePackages {
+			req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, fakePackageName, "1.0.99")).
+				AddBasicAuth(user.Name)
+			MakeRequest(t, req, http.StatusNoContent)
+		}
 	})
 
 	t.Run("RegistrationService", func(t *testing.T) {

From 1f8ac27b31b52791396f198b665a1d6bbdcfd8b3 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 4 Jun 2024 09:14:24 +0200
Subject: [PATCH 093/131] Fix overflow on push notification (#31179)

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

<img width="1301" alt="Screenshot 2024-05-30 at 14 43 24"
src="https://github.com/go-gitea/gitea/assets/115237/00443af0-088d-49a5-be9e-8c9adcc2c01d">
---
 templates/repo/code/recently_pushed_new_branches.tmpl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl
index 7f613fcba7..025cc1a403 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 tw-flex tw-items-center">
-		<div class="tw-flex-1">
+	<div class="ui positive message tw-flex tw-items-center tw-gap-2">
+		<div class="tw-flex-1 tw-break-anywhere">
 			{{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}}
 			{{$branchLink := HTMLFormat `<a href="%s">%s</a>` .BranchLink .BranchDisplayName}}
 			{{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}}

From 4ca65fabdad75e39f9948b9a2a18e32edc98ec02 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 4 Jun 2024 09:46:05 +0200
Subject: [PATCH 094/131] Remove .segment from .project-column (#31204)

Using `.segment` on the project columns is a major abuse of that class,
so remove it and instead set the border-radius directly on it.

Fixes: https://github.com/go-gitea/gitea/issues/31129
---
 templates/projects/view.tmpl      | 2 +-
 web_src/css/features/projects.css | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 45c8461218..6d331caba7 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -66,7 +66,7 @@
 <div id="project-board">
 	<div class="board {{if .CanWriteProjects}}sortable{{end}}"{{if .CanWriteProjects}} data-url="{{$.Link}}/move"{{end}}>
 		{{range .Columns}}
-			<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"{{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 circular label project-column-issue-count">
 						{{.NumIssues ctx}}
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index e25182051a..151b0a23d9 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -9,6 +9,7 @@
 .project-column {
   background-color: var(--color-project-column-bg) !important;
   border: 1px solid var(--color-secondary) !important;
+  border-radius: var(--border-radius);
   margin: 0 0.5rem !important;
   padding: 0.5rem !important;
   width: 320px;

From 90008111181b874ac018455d8d7a2f8bfe6bc71e Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 4 Jun 2024 20:19:41 +0800
Subject: [PATCH 095/131] Make pasted "img" tag has the same behavior as
 markdown image (#31235)

Fix #31230

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 modules/markup/html.go               | 60 ++++++++++++++++++++--------
 modules/markup/html_internal_test.go | 19 +++++----
 modules/markup/html_test.go          | 51 ++++++++++-------------
 modules/markup/renderer.go           |  2 +-
 web_src/js/features/comp/Paste.js    |  6 ++-
 5 files changed, 79 insertions(+), 59 deletions(-)

diff --git a/modules/markup/html.go b/modules/markup/html.go
index 0af74d2680..8dbc958299 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -372,7 +372,42 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
 	return nil
 }
 
-func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
+func handleNodeImg(ctx *RenderContext, img *html.Node) {
+	for i, attr := range img.Attr {
+		if attr.Key != "src" {
+			continue
+		}
+
+		if attr.Val != "" && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "/") {
+			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
+
+			// By default, the "<img>" tag should also be clickable,
+			// because frontend use `<img>` to paste the re-scaled image into the markdown,
+			// so it must match the default markdown image behavior.
+			hasParentAnchor := false
+			for p := img.Parent; p != nil; p = p.Parent {
+				if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
+					break
+				}
+			}
+			if !hasParentAnchor {
+				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
+					{Key: "href", Val: attr.Val},
+					{Key: "target", Val: "_blank"},
+				}}
+				parent := img.Parent
+				imgNext := img.NextSibling
+				parent.RemoveChild(img)
+				parent.InsertBefore(imgA, imgNext)
+				imgA.AppendChild(img)
+			}
+		}
+		attr.Val = camoHandleLink(attr.Val)
+		img.Attr[i] = attr
+	}
+}
+
+func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
 	// Add user-content- to IDs and "#" links if they don't already have them
 	for idx, attr := range node.Attr {
 		val := strings.TrimPrefix(attr.Val, "#")
@@ -397,21 +432,14 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
 		textNode(ctx, procs, node)
 	case html.ElementNode:
 		if node.Data == "img" {
-			for i, attr := range node.Attr {
-				if attr.Key != "src" {
-					continue
-				}
-				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)
-				node.Attr[i] = attr
-			}
+			next := node.NextSibling
+			handleNodeImg(ctx, node)
+			return next
 		} else if node.Data == "a" {
 			// Restrict text in links to emojis
 			procs = emojiProcessors
 		} else if node.Data == "code" || node.Data == "pre" {
-			return
+			return node.NextSibling
 		} else if node.Data == "i" {
 			for _, attr := range node.Attr {
 				if attr.Key != "class" {
@@ -434,11 +462,11 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
 				}
 			}
 		}
-		for n := node.FirstChild; n != nil; n = n.NextSibling {
-			visitNode(ctx, procs, n)
+		for n := node.FirstChild; n != nil; {
+			n = visitNode(ctx, procs, n)
 		}
 	}
-	// ignore everything else
+	return node.NextSibling
 }
 
 // textNode runs the passed node through various processors, in order to handle
@@ -851,7 +879,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 
 	// FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
 	// The "mode" approach should be refactored to some other more clear&reliable way.
-	crossLinkOnly := (ctx.Metas["mode"] == "document" && !ctx.IsWiki)
+	crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
 
 	var (
 		found bool
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 3ff0597851..9aa9c22d70 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -18,8 +18,7 @@ import (
 
 const (
 	TestAppURL  = "http://localhost:3000/"
-	TestOrgRepo = "gogits/gogs"
-	TestRepoURL = TestAppURL + TestOrgRepo + "/"
+	TestRepoURL = TestAppURL + "test-owner/test-repo/"
 )
 
 // externalIssueLink an HTML link to an alphanumeric-style issue
@@ -64,8 +63,8 @@ var regexpMetas = map[string]string{
 
 // these values should match the TestOrgRepo const above
 var localMetas = map[string]string{
-	"user": "gogits",
-	"repo": "gogs",
+	"user": "test-owner",
+	"repo": "test-repo",
 }
 
 func TestRender_IssueIndexPattern(t *testing.T) {
@@ -362,12 +361,12 @@ func TestRender_FullIssueURLs(t *testing.T) {
 		`Look here <a href="http://localhost:3000/person/repo/issues/4" class="ref-issue">person/repo#4</a>`)
 	test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
 		`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
-	test("http://localhost:3000/gogits/gogs/issues/4",
-		`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a>`)
-	test("http://localhost:3000/gogits/gogs/issues/4 test",
-		`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a> test`)
-	test("http://localhost:3000/gogits/gogs/issues/4?a=1&b=2#comment-123 test",
-		`<a href="http://localhost:3000/gogits/gogs/issues/4?a=1&amp;b=2#comment-123" class="ref-issue">#4 (comment)</a> test`)
+	test("http://localhost:3000/test-owner/test-repo/issues/4",
+		`<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a>`)
+	test("http://localhost:3000/test-owner/test-repo/issues/4 test",
+		`<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a> test`)
+	test("http://localhost:3000/test-owner/test-repo/issues/4?a=1&b=2#comment-123 test",
+		`<a href="http://localhost:3000/test-owner/test-repo/issues/4?a=1&amp;b=2#comment-123" class="ref-issue">#4 (comment)</a> test`)
 	test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24",
 		"http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24")
 	test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files",
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index e2d08692e4..df3c2609ef 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -120,8 +120,8 @@ func TestRender_CrossReferences(t *testing.T) {
 	}
 
 	test(
-		"gogits/gogs#12345",
-		`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogits", "gogs", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogits/gogs#12345</a></p>`)
+		"test-owner/test-repo#12345",
+		`<p><a href="`+util.URLJoin(markup.TestAppURL, "test-owner", "test-repo", "issues", "12345")+`" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`)
 	test(
 		"go-gitea/gitea#12345",
 		`<p><a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
@@ -530,43 +530,31 @@ func TestRender_ShortLinks(t *testing.T) {
 }
 
 func TestRender_RelativeImages(t *testing.T) {
-	setting.AppURL = markup.TestAppURL
-
-	test := func(input, expected, expectedWiki string) {
+	render := func(input string, isWiki bool, links markup.Links) string {
 		buffer, err := markdown.RenderString(&markup.RenderContext{
-			Ctx: git.DefaultContext,
-			Links: markup.Links{
-				Base:       markup.TestRepoURL,
-				BranchPath: "master",
-			},
-			Metas: localMetas,
-		}, input)
-		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
-		buffer, err = markdown.RenderString(&markup.RenderContext{
-			Ctx: git.DefaultContext,
-			Links: markup.Links{
-				Base: markup.TestRepoURL,
-			},
+			Ctx:    git.DefaultContext,
+			Links:  links,
 			Metas:  localMetas,
-			IsWiki: true,
+			IsWiki: isWiki,
 		}, input)
 		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
+		return strings.TrimSpace(string(buffer))
 	}
 
-	rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")
-	mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
+	out := render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
+	assert.Equal(t, `<a href="/test-owner/test-repo/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/LINK"/></a>`, out)
 
-	test(
-		`<img src="Link">`,
-		`<img src="`+util.URLJoin(mediatree, "Link")+`"/>`,
-		`<img src="`+util.URLJoin(rawwiki, "Link")+`"/>`)
+	out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
+	assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
 
-	test(
-		`<img src="./icon.png">`,
-		`<img src="`+util.URLJoin(mediatree, "icon.png")+`"/>`,
-		`<img src="`+util.URLJoin(rawwiki, "icon.png")+`"/>`)
+	out = render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
+	assert.Equal(t, `<a href="/test-owner/test-repo/media/test-branch/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/media/test-branch/LINK"/></a>`, out)
+
+	out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
+	assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
+
+	out = render(`<img src="/LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
+	assert.Equal(t, `<img src="/LINK"/>`, out)
 }
 
 func Test_ParseClusterFuzz(t *testing.T) {
@@ -719,5 +707,6 @@ func TestIssue18471(t *testing.T) {
 func TestIsFullURL(t *testing.T) {
 	assert.True(t, markup.IsFullURLString("https://example.com"))
 	assert.True(t, markup.IsFullURLString("mailto:test@example.com"))
+	assert.True(t, markup.IsFullURLString("data:image/11111"))
 	assert.False(t, markup.IsFullURLString("/foo:bar"))
 }
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 44dedf638b..66e8cf611d 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -74,7 +74,7 @@ type RenderContext struct {
 	Type             string
 	IsWiki           bool
 	Links            Links
-	Metas            map[string]string
+	Metas            map[string]string // user, repo, mode(comment/document)
 	DefaultLink      string
 	GitRepo          *git.Repository
 	Repo             gitrepo.Repository
diff --git a/web_src/js/features/comp/Paste.js b/web_src/js/features/comp/Paste.js
index b26296d1fc..35a7ceaef8 100644
--- a/web_src/js/features/comp/Paste.js
+++ b/web_src/js/features/comp/Paste.js
@@ -100,13 +100,17 @@ async function handleClipboardImages(editor, dropzone, images, e) {
     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.
+      // Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}"
+      const url = `attachments/${uuid}`;
       text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
     } else {
+      // Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}"
+      // TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments"
+      const url = `/attachments/${uuid}`;
       text = `![${name}](${url})`;
     }
     editor.replacePlaceholder(placeholder, text);

From 138e946c3d8e2731f11a3e3b6876889694822f46 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 4 Jun 2024 15:57:11 +0200
Subject: [PATCH 096/131] Replace `gt-word-break` with `tw-break-anywhere`
 (#31183)

`overflow-wrap: anywhere` is a superior alternative to `word-wrap:
break-word` and we were already setting it in the class. I tested a few
cases, all look good.
---
 docs/content/contributing/guidelines-frontend.en-us.md | 2 +-
 docs/content/contributing/guidelines-frontend.zh-cn.md | 2 +-
 templates/admin/repo/list.tmpl                         | 4 ++--
 templates/package/content/container.tmpl               | 4 ++--
 templates/package/settings.tmpl                        | 2 +-
 templates/projects/view.tmpl                           | 2 +-
 templates/repo/home.tmpl                               | 2 +-
 templates/repo/issue/list.tmpl                         | 2 +-
 templates/repo/issue/view_content/conversation.tmpl    | 2 +-
 templates/repo/release/list.tmpl                       | 2 +-
 templates/repo/settings/options.tmpl                   | 2 +-
 templates/repo/wiki/revision.tmpl                      | 2 +-
 templates/shared/user/org_profile_avatar.tmpl          | 2 +-
 templates/shared/user/profile_big_avatar.tmpl          | 4 ++--
 web_src/css/helpers.css                                | 5 -----
 web_src/js/features/repo-issue.js                      | 2 +-
 16 files changed, 18 insertions(+), 23 deletions(-)

diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md
index efeaf38bb2..a08098a931 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-word-break`), 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-ellipsis`), 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 394097b259..198e1227e5 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-word-break`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
+12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-ellipsis`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
 13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
 
 ### 可访问性 / ARIA
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index 4b27d87a45..69031e42eb 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -47,13 +47,13 @@
 						<tr>
 							<td>{{.ID}}</td>
 							<td>
-								<a class="gt-word-break" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>
+								<a class="tw-break-anywhere" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>
 								{{if .Owner.Visibility.IsPrivate}}
 									<span class="text gold">{{svg "octicon-lock"}}</span>
 								{{end}}
 							</td>
 							<td>
-								<a class="gt-word-break" href="{{.Link}}">{{.Name}}</a>
+								<a class="tw-break-anywhere" href="{{.Link}}">{{.Name}}</a>
 								{{if .IsArchived}}
 									<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
 								{{end}}
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index fe393f4388..138fedecb3 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -54,7 +54,7 @@
 	{{end}}
 	{{if .PackageDescriptor.Metadata.ImageLayers}}
 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.layers"}}</h4>
-		<div class="ui attached segment gt-word-break">
+		<div class="ui attached segment tw-break-anywhere">
 			<table class="ui very basic compact table">
 				<tbody>
 					{{range .PackageDescriptor.Metadata.ImageLayers}}
@@ -80,7 +80,7 @@
 					{{range $key, $value := .PackageDescriptor.Metadata.Labels}}
 						<tr>
 							<td class="top aligned">{{$key}}</td>
-							<td class="gt-word-break">{{$value}}</td>
+							<td class="tw-break-anywhere">{{$value}}</td>
 						</tr>
 					{{end}}
 				</tbody>
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl
index 9424baf493..4b8773477b 100644
--- a/templates/package/settings.tmpl
+++ b/templates/package/settings.tmpl
@@ -59,7 +59,7 @@
 							{{ctx.Locale.Tr "packages.settings.delete"}}
 						</div>
 						<div class="content">
-							<div class="ui warning message gt-word-break">
+							<div class="ui warning message tw-break-anywhere">
 								{{ctx.Locale.Tr "packages.settings.delete.notice" .PackageDescriptor.Package.Name .PackageDescriptor.Version.Version}}
 							</div>
 							<form class="ui form" action="{{.Link}}" method="post">
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 6d331caba7..584462d2a2 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -152,7 +152,7 @@
 				<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}}">
+						<div class="issue-card tw-break-anywhere {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}">
 							{{template "repo/issue/card" (dict "Issue" . "Page" $)}}
 						</div>
 					{{end}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index ef76f3ed5d..ff82f2ca80 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -5,7 +5,7 @@
 		{{template "base/alert" .}}
 		{{template "repo/code/recently_pushed_new_branches" .}}
 		{{if and (not .HideRepoInfo) (not .IsBlame)}}
-		<div class="repo-description gt-word-break">
+		<div class="repo-description tw-break-anywhere">
 			{{- $description := .Repository.DescriptionHTML ctx -}}
 			{{if $description}}{{$description | RenderCodeBlock}}{{end}}
 			{{if .Repository.Website}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 30edf825f1..01b610b39d 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -7,7 +7,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}}tw-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}">
+				<div class="issue-card tw-break-anywhere {{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/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl
index 43ec9d75c4..ccea9b690d 100644
--- a/templates/repo/issue/view_content/conversation.tmpl
+++ b/templates/repo/issue/view_content/conversation.tmpl
@@ -8,7 +8,7 @@
 	<div class="ui segments conversation-holder">
 		<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 tw-ml-2 gt-word-break">{{$comment.TreePath}}</a>
+				<a href="{{$comment.CodeCommentLink ctx}}" class="file-comment tw-ml-2 tw-break-anywhere">{{$comment.TreePath}}</a>
 				{{if $invalid}}
 					<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"}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 34548672b5..e5bf23faac 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -17,7 +17,7 @@
 					</div>
 					<div class="ui segment detail">
 						<div class="tw-flex tw-items-center tw-justify-between tw-flex-wrap tw-mb-2">
-							<h4 class="release-list-title gt-word-break">
+							<h4 class="release-list-title tw-break-anywhere">
 								{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a class="muted" 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"}}
 								{{if $release.IsDraft}}
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 6c49f00094..4f98133df3 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -217,7 +217,7 @@
 						<tbody>
 							{{range .PushMirrors}}
 							<tr>
-								<td class="gt-word-break">{{.RemoteAddress}}</td>
+								<td class="tw-break-anywhere">{{.RemoteAddress}}</td>
 								<td>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.push"}}</td>
 								<td>{{if .LastUpdateUnix}}{{DateTime "full" .LastUpdateUnix}}{{else}}{{ctx.Locale.Tr "never"}}{{end}} {{if .LastError}}<div class="ui red label" data-tooltip-content="{{.LastError}}">{{ctx.Locale.Tr "error"}}</div>{{end}}</td>
 								<td class="right aligned">
diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
index 8e0060d4b3..7fca703843 100644
--- a/templates/repo/wiki/revision.tmpl
+++ b/templates/repo/wiki/revision.tmpl
@@ -8,7 +8,7 @@
 				<div class="ui header">
 					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}"><span>{{.revision}}</span> {{svg "octicon-home"}}</a>
 					{{$title}}
-					<div class="ui sub header gt-word-break">
+					<div class="ui sub header tw-break-anywhere">
 						{{$timeSince := TimeSince .Author.When ctx.Locale}}
 						{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
 					</div>
diff --git a/templates/shared/user/org_profile_avatar.tmpl b/templates/shared/user/org_profile_avatar.tmpl
index d67f133abf..c0abcabff1 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-items-center gt-word-break">
+				<div class="ui header tw-flex tw-items-center tw-break-anywhere">
 					{{ctx.AvatarUtils.Avatar . 100}}
 					<span class="text grey"><a class="muted" 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 868f8d5a13..29c6eb0eb0 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -11,7 +11,7 @@
 		</span>
 	{{end}}
 	</div>
-	<div class="content gt-word-break profile-avatar-name">
+	<div class="content tw-break-anywhere profile-avatar-name">
 		{{if .ContextUser.FullName}}<span class="header text center">{{.ContextUser.FullName}}</span>{{end}}
 		<span class="username text center">{{.ContextUser.Name}} {{if .IsAdmin}}
 					<a class="muted" href="{{AppSubUrl}}/admin/users/{{.ContextUser.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.users.details"}}">
@@ -25,7 +25,7 @@
 			{{end}}
 		</div>
 	</div>
-	<div class="extra content gt-word-break">
+	<div class="extra content tw-break-anywhere">
 		<ul>
 			{{if .UserBlocking}}
 				<li class="text red">{{svg "octicon-circle-slash"}} {{ctx.Locale.Tr "user.block.blocked"}}</li>
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index 15df9f3a45..42d06e2e66 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-word-break {
-  word-wrap: break-word !important;
-  overflow-wrap: anywhere;
-}
-
 .gt-ellipsis {
   overflow: hidden !important;
   white-space: nowrap !important;
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 519db34934..95910e34bc 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -125,7 +125,7 @@ export function initRepoIssueSidebarList() {
             }
             filteredResponse.results.push({
               name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div>
-<div class="text small gt-word-break">${htmlEscape(issue.repository.full_name)}</div>`,
+<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
               value: issue.id,
             });
           });

From fcc061ae4435f251d14a6750a0f5713800dca637 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 4 Jun 2024 23:06:21 +0800
Subject: [PATCH 097/131] Fix admin oauth2 custom URL settings (#31246)

Fix #31244
---
 web_src/js/features/admin/common.js | 29 +++++++++++++++++------------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index b35502d52f..3c90b546b8 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -67,39 +67,44 @@ export function initAdminCommon() {
       input.removeAttribute('required');
     }
 
-    const provider = document.getElementById('oauth2_provider')?.value;
+    const provider = document.getElementById('oauth2_provider').value;
     switch (provider) {
       case 'openidConnect':
-        for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input')) {
-          input.setAttribute('required', 'required');
-        }
+        document.querySelector('.open_id_connect_auto_discovery_url input').setAttribute('required', 'required');
         showElem('.open_id_connect_auto_discovery_url');
         break;
-      default:
-        if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-required')) {
-          document.getElementById('oauth2_use_custom_url')?.setAttribute('checked', 'checked');
+      default: {
+        const elProviderCustomUrlSettings = document.querySelector(`#${provider}_customURLSettings`);
+        if (!elProviderCustomUrlSettings) break; // some providers do not have custom URL settings
+        const couldChangeCustomURLs = elProviderCustomUrlSettings.getAttribute('data-available') === 'true';
+        const mustProvideCustomURLs = elProviderCustomUrlSettings.getAttribute('data-required') === 'true';
+        if (couldChangeCustomURLs) {
+          showElem('.oauth2_use_custom_url'); // show the checkbox
         }
-        if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-available')) {
-          showElem('.oauth2_use_custom_url');
+        if (mustProvideCustomURLs) {
+          document.querySelector('#oauth2_use_custom_url').checked = true; // make the checkbox checked
         }
+        break;
+      }
     }
     onOAuth2UseCustomURLChange(applyDefaultValues);
   }
 
   function onOAuth2UseCustomURLChange(applyDefaultValues) {
-    const provider = document.getElementById('oauth2_provider')?.value;
+    const provider = document.getElementById('oauth2_provider').value;
     hideElem('.oauth2_use_custom_url_field');
     for (const input of document.querySelectorAll('.oauth2_use_custom_url_field input[required]')) {
       input.removeAttribute('required');
     }
 
-    if (document.getElementById('oauth2_use_custom_url')?.checked) {
+    const elProviderCustomUrlSettings = document.querySelector(`#${provider}_customURLSettings`);
+    if (elProviderCustomUrlSettings && document.getElementById('oauth2_use_custom_url').checked) {
       for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
         if (applyDefaultValues) {
           document.getElementById(`oauth2_${custom}`).value = document.getElementById(`${provider}_${custom}`).value;
         }
         const customInput = document.getElementById(`${provider}_${custom}`);
-        if (customInput && customInput.getAttribute('data-available')) {
+        if (customInput && customInput.getAttribute('data-available') === 'true') {
           for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) {
             input.setAttribute('required', 'required');
           }

From bd80225ec3688cfa89767cc352835d8d5093f764 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 4 Jun 2024 23:35:29 +0800
Subject: [PATCH 098/131] Make blockquote attention recognize more syntaxes
 (#31240)

Fix #31214
---
 modules/markup/markdown/markdown_test.go      |  6 ++
 modules/markup/markdown/math/block_parser.go  | 10 +-
 .../markup/markdown/transform_blockquote.go   | 91 +++++++++++++++----
 3 files changed, 87 insertions(+), 20 deletions(-)

diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index b4a7efa8dd..8c41ec12e3 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -1019,4 +1019,10 @@ func TestAttention(t *testing.T) {
 	test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>")
 	test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
 	test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\n</blockquote>")
+
+	// escaped by mdformat
+	test(`> \[!NOTE\]`, renderAttention("note", "octicon-info")+"\n</blockquote>")
+
+	// legacy GitHub style
+	test(`> **warning**`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
 }
diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go
index 7f714d7239..37f6caf11c 100644
--- a/modules/markup/markdown/math/block_parser.go
+++ b/modules/markup/markdown/math/block_parser.go
@@ -31,10 +31,16 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
 		return nil, parser.NoChildren
 	}
 
-	dollars := false
+	var dollars bool
 	if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' {
 		dollars = true
-	} else if line[pos] != '\\' || line[pos+1] != '[' {
+	} else if line[pos] == '\\' && line[pos+1] == '[' {
+		if len(line[pos:]) >= 3 && line[pos+2] == '!' && bytes.Contains(line[pos:], []byte(`\]`)) {
+			// do not process escaped attention block: "> \[!NOTE\]"
+			return nil, parser.NoChildren
+		}
+		dollars = false
+	} else {
 		return nil, parser.NoChildren
 	}
 
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index 933f0e5c59..d2dc025052 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -15,7 +15,7 @@ import (
 	"golang.org/x/text/language"
 )
 
-// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
+// 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)
@@ -37,38 +37,93 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast
 	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("]")
+func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
+	if firstParagraph.ChildCount() < 1 {
+		return "", nil
+	}
+	node1, ok := firstParagraph.FirstChild().(*ast.Emphasis)
+	if !ok {
+		return "", nil
+	}
+	val1 := string(node1.Text(reader.Source()))
+	attentionType := strings.ToLower(val1)
+	if g.attentionTypes.Contains(attentionType) {
+		return attentionType, []ast.Node{node1}
+	}
+	return "", nil
+}
 
-	// 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
+func (g *ASTTransformer) extractBlockquoteAttention2(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
+	if firstParagraph.ChildCount() < 2 {
+		return "", nil
 	}
 	node1, ok := firstParagraph.FirstChild().(*ast.Text)
 	if !ok {
-		return ast.WalkContinue, nil
+		return "", nil
 	}
 	node2, ok := node1.NextSibling().(*ast.Text)
 	if !ok {
-		return ast.WalkContinue, nil
+		return "", nil
+	}
+	val1 := string(node1.Segment.Value(reader.Source()))
+	val2 := string(node2.Segment.Value(reader.Source()))
+	if strings.HasPrefix(val1, `\[!`) && val2 == `\]` {
+		attentionType := strings.ToLower(val1[3:])
+		if g.attentionTypes.Contains(attentionType) {
+			return attentionType, []ast.Node{node1, node2}
+		}
+	}
+	return "", nil
+}
+
+func (g *ASTTransformer) extractBlockquoteAttention3(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
+	if firstParagraph.ChildCount() < 3 {
+		return "", nil
+	}
+	node1, ok := firstParagraph.FirstChild().(*ast.Text)
+	if !ok {
+		return "", nil
+	}
+	node2, ok := node1.NextSibling().(*ast.Text)
+	if !ok {
+		return "", nil
 	}
 	node3, ok := node2.NextSibling().(*ast.Text)
 	if !ok {
-		return ast.WalkContinue, nil
+		return "", 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
+		return "", nil
 	}
 
-	// grab attention type from markdown source
 	attentionType := strings.ToLower(val2[1:])
-	if !g.attentionTypes.Contains(attentionType) {
+	if g.attentionTypes.Contains(attentionType) {
+		return attentionType, []ast.Node{node1, node2, node3}
+	}
+	return "", 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("]")
+	// > Text("\[!TYPE") TEXT("\]")
+	// > Text("**TYPE**")
+
+	// grab these nodes and make sure we adhere to the attention blockquote structure
+	firstParagraph := v.FirstChild()
+	g.applyElementDir(firstParagraph)
+
+	attentionType, processedNodes := g.extractBlockquoteAttentionEmphasis(firstParagraph, reader)
+	if attentionType == "" {
+		attentionType, processedNodes = g.extractBlockquoteAttention2(firstParagraph, reader)
+	}
+	if attentionType == "" {
+		attentionType, processedNodes = g.extractBlockquoteAttention3(firstParagraph, reader)
+	}
+	if attentionType == "" {
 		return ast.WalkContinue, nil
 	}
 
@@ -88,9 +143,9 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
 	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)
+	for _, processed := range processedNodes {
+		firstParagraph.RemoveChild(firstParagraph, processed)
+	}
 	if firstParagraph.ChildCount() == 0 {
 		firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
 	}

From 816222243af523316041692622be6f48ef068693 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 5 Jun 2024 03:22:38 +0200
Subject: [PATCH 099/131] Add `lint-go-gopls` (#30729)

Uses `gopls check <files>` as a linter. Tested locally and brings up 149
errors currently for me. I don't think I want to fix them in this PR,
but I would like at least to get this analysis running on CI.

List of errors:
```
modules/indexer/code/indexer.go:181:11: impossible condition: nil != nil
routers/private/hook_post_receive.go:120:15: tautological condition: nil == nil
services/auth/source/oauth2/providers.go:185:9: tautological condition: nil == nil
services/convert/issue.go:216:11: tautological condition: non-nil != nil
tests/integration/git_test.go:332:9: impossible condition: nil != nil
services/migrations/migrate.go:179:24-43: unused parameter: ctx
services/repository/transfer.go:288:48-69: unused parameter: doer
tests/integration/api_repo_tags_test.go:75:41-61: unused parameter: session
tests/integration/git_test.go:696:64-74: unused parameter: baseBranch
tests/integration/gpg_git_test.go:265:27-39: unused parameter: t
tests/integration/gpg_git_test.go:284:23-29: unused parameter: tmpDir
tests/integration/gpg_git_test.go:284:31-35: unused parameter: name
tests/integration/gpg_git_test.go:284:37-42: unused parameter: email
```
---
 Makefile                                | 10 +++++++++-
 services/migrations/migrate.go          |  2 +-
 services/repository/transfer.go         |  4 ++--
 tests/integration/api_repo_tags_test.go |  4 ++--
 tests/integration/dump_restore_test.go  |  2 +-
 tests/integration/gpg_git_test.go       |  6 +++---
 tools/lint-go-gopls.sh                  | 23 +++++++++++++++++++++++
 7 files changed, 41 insertions(+), 10 deletions(-)
 create mode 100755 tools/lint-go-gopls.sh

diff --git a/Makefile b/Makefile
index e9dc945206..d97360c9f4 100644
--- a/Makefile
+++ b/Makefile
@@ -36,6 +36,7 @@ XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
 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
+GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3
 
 DOCKER_IMAGE ?= gitea/gitea
 DOCKER_TAG ?= latest
@@ -213,6 +214,7 @@ help:
 	@echo " - lint-go                          lint go files"
 	@echo " - lint-go-fix                      lint go files and fix issues"
 	@echo " - lint-go-vet                      lint go files with vet"
+	@echo " - lint-go-gopls                    lint go files with gopls"
 	@echo " - lint-js                          lint js files"
 	@echo " - lint-js-fix                      lint js files and fix issues"
 	@echo " - lint-css                         lint css files"
@@ -366,7 +368,7 @@ lint-frontend: lint-js lint-css
 lint-frontend-fix: lint-js-fix lint-css-fix
 
 .PHONY: lint-backend
-lint-backend: lint-go lint-go-vet lint-editorconfig
+lint-backend: lint-go lint-go-vet lint-go-gopls lint-editorconfig
 
 .PHONY: lint-backend-fix
 lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
@@ -424,6 +426,11 @@ lint-go-vet:
 	@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
 	@$(GO) vet -vettool=gitea-vet ./...
 
+.PHONY: lint-go-gopls
+lint-go-gopls:
+	@echo "Running gopls check..."
+	@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA)
+
 .PHONY: lint-editorconfig
 lint-editorconfig:
 	@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
@@ -864,6 +871,7 @@ deps-tools:
 	$(GO) install $(GO_LICENSES_PACKAGE)
 	$(GO) install $(GOVULNCHECK_PACKAGE)
 	$(GO) install $(ACTIONLINT_PACKAGE)
+	$(GO) install $(GOPLS_PACKAGE)
 
 node_modules: package-lock.json
 	npm install --no-save
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index 5bb3056161..21bdc68e73 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -176,7 +176,7 @@ func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptio
 // migrateRepository will download information and then upload it to Uploader, this is a simple
 // process for small repository. For a big repository, save all the data to disk
 // before upload is better
-func migrateRepository(ctx context.Context, doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
+func migrateRepository(_ context.Context, doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
 	if messenger == nil {
 		messenger = base.NilMessenger
 	}
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index 3d0bce18d0..9e0ff7ae14 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -285,7 +285,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
 }
 
 // changeRepositoryName changes all corresponding setting from old repository name to new one.
-func changeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newRepoName string) (err error) {
+func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newRepoName string) (err error) {
 	oldRepoName := repo.Name
 	newRepoName = strings.ToLower(newRepoName)
 	if err = repo_model.IsUsableRepoName(newRepoName); err != nil {
@@ -347,7 +347,7 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo
 	// local copy's origin accordingly.
 
 	repoWorkingPool.CheckIn(fmt.Sprint(repo.ID))
-	if err := changeRepositoryName(ctx, doer, repo, newRepoName); err != nil {
+	if err := changeRepositoryName(ctx, repo, newRepoName); err != nil {
 		repoWorkingPool.CheckOut(fmt.Sprint(repo.ID))
 		return err
 	}
diff --git a/tests/integration/api_repo_tags_test.go b/tests/integration/api_repo_tags_test.go
index c6eeb404c0..a7f021ca4f 100644
--- a/tests/integration/api_repo_tags_test.go
+++ b/tests/integration/api_repo_tags_test.go
@@ -42,7 +42,7 @@ func TestAPIRepoTags(t *testing.T) {
 	assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.zip", tags[0].ZipballURL)
 	assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.tar.gz", tags[0].TarballURL)
 
-	newTag := createNewTagUsingAPI(t, session, token, user.Name, repoName, "gitea/22", "", "nice!\nand some text")
+	newTag := createNewTagUsingAPI(t, token, user.Name, repoName, "gitea/22", "", "nice!\nand some text")
 	resp = MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &tags)
 	assert.Len(t, tags, 2)
@@ -72,7 +72,7 @@ func TestAPIRepoTags(t *testing.T) {
 	MakeRequest(t, req, http.StatusNotFound)
 }
 
-func createNewTagUsingAPI(t *testing.T, session *TestSession, token, ownerName, repoName, name, target, msg string) *api.Tag {
+func createNewTagUsingAPI(t *testing.T, token, ownerName, repoName, name, target, msg string) *api.Tag {
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags", ownerName, repoName)
 	req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateTagOption{
 		TagName: name,
diff --git a/tests/integration/dump_restore_test.go b/tests/integration/dump_restore_test.go
index bed2453054..47bb6f76e9 100644
--- a/tests/integration/dump_restore_test.go
+++ b/tests/integration/dump_restore_test.go
@@ -237,7 +237,7 @@ func (c *compareDump) assertLoadFiles(beforeFilename, afterFilename string, t re
 		//
 		// Given []Something{} create afterPtr, beforePtr []*Something{}
 		//
-		sliceType := reflect.SliceOf(reflect.PtrTo(t.Elem()))
+		sliceType := reflect.SliceOf(reflect.PointerTo(t.Elem()))
 		beforeSlice := reflect.MakeSlice(sliceType, 0, 10)
 		beforePtr = reflect.New(beforeSlice.Type())
 		beforePtr.Elem().Set(beforeSlice)
diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go
index 3ba4a5882c..047c049c7f 100644
--- a/tests/integration/gpg_git_test.go
+++ b/tests/integration/gpg_git_test.go
@@ -35,7 +35,7 @@ func TestGPGGit(t *testing.T) {
 	defer os.Setenv("GNUPGHOME", oldGNUPGHome)
 
 	// Need to create a root key
-	rootKeyPair, err := importTestingKey(tmpDir, "gitea", "gitea@fake.local")
+	rootKeyPair, err := importTestingKey()
 	if !assert.NoError(t, err, "importTestingKey") {
 		return
 	}
@@ -262,7 +262,7 @@ func TestGPGGit(t *testing.T) {
 	})
 }
 
-func crudActionCreateFile(t *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
+func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
 	return doAPICreateFile(ctx, path, &api.CreateFileOptions{
 		FileOptions: api.FileOptions{
 			BranchName:    from,
@@ -281,7 +281,7 @@ func crudActionCreateFile(t *testing.T, ctx APITestContext, user *user_model.Use
 	}, callback...)
 }
 
-func importTestingKey(tmpDir, name, email string) (*openpgp.Entity, error) {
+func importTestingKey() (*openpgp.Entity, error) {
 	if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil {
 		return nil, err
 	}
diff --git a/tools/lint-go-gopls.sh b/tools/lint-go-gopls.sh
new file mode 100755
index 0000000000..4bb69f4c16
--- /dev/null
+++ b/tools/lint-go-gopls.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+set -uo pipefail
+
+cd "$(dirname -- "${BASH_SOURCE[0]}")" && cd ..
+
+IGNORE_PATTERNS=(
+  "is deprecated" # TODO: fix these
+)
+
+# lint all go files with 'gopls check' and look for lines starting with the
+# current absolute path, indicating a error was found. This is neccessary
+# because the tool does not set non-zero exit code when errors are found.
+# ref: https://github.com/golang/go/issues/67078
+ERROR_LINES=$("$GO" run "$GOPLS_PACKAGE" check "$@" 2>/dev/null | grep -E "^$PWD" | grep -vFf <(printf '%s\n' "${IGNORE_PATTERNS[@]}"));
+NUM_ERRORS=$(echo -n "$ERROR_LINES" | wc -l)
+
+if [ "$NUM_ERRORS" -eq "0" ]; then
+  exit 0;
+else
+  echo "$ERROR_LINES"
+  echo "Found $NUM_ERRORS 'gopls check' errors"
+  exit 1;
+fi

From 8de8972baf5d82ff7b58ed77d78e8e1869e64eb5 Mon Sep 17 00:00:00 2001
From: Rowan Bohde <rowan.bohde@gmail.com>
Date: Tue, 4 Jun 2024 23:00:56 -0500
Subject: [PATCH 100/131] fix: allow actions artifacts storage migration to
 complete succesfully (#31251)

Change the copy to use `ActionsArtifact.StoragePath` instead of the
`ArtifactPath`. Skip artifacts that are expired, and don't error if the
file to copy does not exist.

---

When trying to migrate actions artifact storage from local to MinIO, we
encountered errors that prevented the process from completing
successfully:

* The migration tries to copy the files using the per-run
`ArtifactPath`, instead of the unique `StoragePath`.
* Artifacts that have been marked expired and had their files deleted
would throw an error
* Artifacts that are pending, but don't have a file uploaded yet will
throw an error.

This PR addresses these cases, and allow the process to complete
successfully.
---
 cmd/migrate_storage.go | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go
index 1720b6fb53..6ece4bf661 100644
--- a/cmd/migrate_storage.go
+++ b/cmd/migrate_storage.go
@@ -5,7 +5,9 @@ package cmd
 
 import (
 	"context"
+	"errors"
 	"fmt"
+	"io/fs"
 	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
@@ -194,8 +196,20 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
 
 func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
 	return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
-		_, err := storage.Copy(dstStorage, artifact.ArtifactPath, storage.ActionsArtifacts, artifact.ArtifactPath)
-		return err
+		if artifact.Status == int64(actions_model.ArtifactStatusExpired) {
+			return nil
+		}
+
+		_, err := storage.Copy(dstStorage, artifact.StoragePath, storage.ActionsArtifacts, artifact.StoragePath)
+		if err != nil {
+			// ignore files that do not exist
+			if errors.Is(err, fs.ErrNotExist) {
+				return nil
+			}
+			return err
+		}
+
+		return nil
 	})
 }
 

From 06ebae7472aef4380602d2ecd64fdc9dddcb6037 Mon Sep 17 00:00:00 2001
From: Kerwin Bryant <kerwin612@qq.com>
Date: Wed, 5 Jun 2024 22:39:45 +0800
Subject: [PATCH 101/131] Optimize runner-tags layout to enhance visual
 experience (#31258)

![image](https://github.com/go-gitea/gitea/assets/3371163/b8199005-94f2-45be-8ca9-4fa1b3f221b2)
---
 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 8163007993..d3a86fe3fa 100644
--- a/templates/shared/actions/runner_list.tmpl
+++ b/templates/shared/actions/runner_list.tmpl
@@ -70,7 +70,7 @@
 						<td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td>
 						<td>{{if .Version}}{{.Version}}{{else}}{{ctx.Locale.Tr "unknown"}}{{end}}</td>
 						<td><span data-tooltip-content="{{.BelongsToOwnerName}}">{{.BelongsToOwnerType.LocaleString ctx.Locale}}</span></td>
-						<td class="runner-tags">
+						<td class="tw-flex tw-flex-wrap tw-gap-2 runner-tags">
 							{{range .AgentLabels}}<span class="ui label">{{.}}</span>{{end}}
 						</td>
 						<td>{{if .LastOnline}}{{TimeSinceUnix .LastOnline ctx.Locale}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</td>

From e728fd741be7848d476663eec1c9caaf34b46e61 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 6 Jun 2024 10:28:33 +0800
Subject: [PATCH 102/131] Fix Activity Page Contributors dropdown (#31264)

Fix #31261
---
 routers/web/repo/contributors.go           |  6 ------
 templates/repo/contributors.tmpl           |  1 +
 web_src/js/components/RepoContributors.vue | 21 ++++++++++-----------
 web_src/js/features/contributors.js        |  1 +
 4 files changed, 12 insertions(+), 17 deletions(-)

diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
index 5fda17469e..762fbf9379 100644
--- a/routers/web/repo/contributors.go
+++ b/routers/web/repo/contributors.go
@@ -19,14 +19,8 @@ const (
 // Contributors render the page to show repository contributors graph
 func Contributors(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors")
-
 	ctx.Data["PageIsActivity"] = true
 	ctx.Data["PageIsContributors"] = true
-
-	ctx.PageData["contributionType"] = "commits"
-
-	ctx.PageData["repoLink"] = ctx.Repo.RepoLink
-
 	ctx.HTML(http.StatusOK, tplContributors)
 }
 
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl
index 54e3e426a2..6b8a63fe99 100644
--- a/templates/repo/contributors.tmpl
+++ b/templates/repo/contributors.tmpl
@@ -1,5 +1,6 @@
 {{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
 	<div id="repo-contributors-chart"
+		data-repo-link="{{.RepoLink}}"
 		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"}}"
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index f7b05831e0..dec2599c0d 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -23,8 +23,6 @@ import {sleep} from '../utils.js';
 import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
 import $ from 'jquery';
 
-const {pageData} = window.config;
-
 const customEventListener = {
   id: 'customEventListener',
   afterEvent: (chart, args, opts) => {
@@ -59,14 +57,17 @@ export default {
       type: Object,
       required: true,
     },
+    repoLink: {
+      type: String,
+      required: true,
+    },
   },
   data: () => ({
     isLoading: false,
     errorText: '',
     totalStats: {},
     sortedContributors: {},
-    repoLink: pageData.repoLink || [],
-    type: pageData.contributionType,
+    type: 'commits',
     contributorsStats: [],
     xAxisStart: null,
     xAxisEnd: null,
@@ -333,19 +334,17 @@ export default {
         <!-- Contribution type -->
         <div class="ui dropdown jump" id="repo-contributors">
           <div class="ui basic compact button">
-            <span class="text">
-              <span class="not-mobile">{{ locale.filterLabel }}&nbsp;</span><strong>{{ locale.contributionType[type] }}</strong>
-              <svg-icon name="octicon-triangle-down" :size="14"/>
-            </span>
+            <span class="not-mobile">{{ locale.filterLabel }}</span> <strong>{{ locale.contributionType[type] }}</strong>
+            <svg-icon name="octicon-triangle-down" :size="14"/>
           </div>
           <div class="menu">
-            <div :class="['item', {'active': type === 'commits'}]">
+            <div :class="['item', {'selected': type === 'commits'}]" data-value="commits">
               {{ locale.contributionType.commits }}
             </div>
-            <div :class="['item', {'active': type === 'additions'}]">
+            <div :class="['item', {'selected': type === 'additions'}]" data-value="additions">
               {{ locale.contributionType.additions }}
             </div>
-            <div :class="['item', {'active': type === 'deletions'}]">
+            <div :class="['item', {'selected': type === 'deletions'}]" data-value="deletions">
               {{ locale.contributionType.deletions }}
             </div>
           </div>
diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js
index 1d9cba5b9b..79b3389fee 100644
--- a/web_src/js/features/contributors.js
+++ b/web_src/js/features/contributors.js
@@ -7,6 +7,7 @@ export async function initRepoContributors() {
   const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
   try {
     const View = createApp(RepoContributors, {
+      repoLink: el.getAttribute('data-repo-link'),
       locale: {
         filterLabel: el.getAttribute('data-locale-filter-label'),
         contributionType: {

From 6a3c487d0734617e0709c96a35394fdd80d9f919 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 6 Jun 2024 05:37:08 +0200
Subject: [PATCH 103/131] Add replacement module for `mholt/archiver` (#31267)

Switch to this fork tag:
https://github.com/anchore/archiver/releases/tag/v3.5.2 which includes
https://github.com/anchore/archiver/commit/82ca88a2eb24d418c30bf960ef071b0bbec04631.

Ref: https://pkg.go.dev/vuln/GO-2024-2698
Ref: https://github.com/advisories/GHSA-rhh4-rh7c-7r5v

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 go.mod | 3 +++
 go.sum | 4 ++--
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 6f739ed6e9..b3a888fcea 100644
--- a/go.mod
+++ b/go.mod
@@ -311,6 +311,9 @@ 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
 
+// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
+replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
+
 exclude github.com/gofrs/uuid v3.2.0+incompatible
 
 exclude github.com/gofrs/uuid v4.0.0+incompatible
diff --git a/go.sum b/go.sum
index 543bd70866..51e57075c3 100644
--- a/go.sum
+++ b/go.sum
@@ -92,6 +92,8 @@ 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/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA=
+github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
 github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
@@ -564,8 +566,6 @@ github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyq
 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=
-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=

From 24dace8f76a8166d48203ed41fd1c3d66ace715c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 6 Jun 2024 06:29:42 +0200
Subject: [PATCH 104/131] Update `golang.org/x/net` (#31260)

Result of `go get -u golang.org/x/net && make tidy`. ~~Fixes
https://pkg.go.dev/vuln/GO-2024-2887.~~
---
 go.mod | 14 +++++++-------
 go.sum | 31 ++++++++++++++++---------------
 2 files changed, 23 insertions(+), 22 deletions(-)

diff --git a/go.mod b/go.mod
index b3a888fcea..ed9d806a65 100644
--- a/go.mod
+++ b/go.mod
@@ -108,13 +108,13 @@ 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.22.0
+	golang.org/x/crypto v0.24.0
 	golang.org/x/image v0.15.0
-	golang.org/x/net v0.24.0
+	golang.org/x/net v0.26.0
 	golang.org/x/oauth2 v0.18.0
-	golang.org/x/sys v0.19.0
-	golang.org/x/text v0.14.0
-	golang.org/x/tools v0.19.0
+	golang.org/x/sys v0.21.0
+	golang.org/x/text v0.16.0
+	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
 	google.golang.org/grpc v1.62.1
 	google.golang.org/protobuf v1.33.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
@@ -293,8 +293,8 @@ require (
 	go.uber.org/multierr v1.11.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/mod v0.17.0 // indirect
+	golang.org/x/sync v0.7.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-20240314234333-6e1732d8331c // indirect
diff --git a/go.sum b/go.sum
index 51e57075c3..11deacf916 100644
--- a/go.sum
+++ b/go.sum
@@ -866,8 +866,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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 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=
@@ -878,8 +878,8 @@ 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.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
-golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -900,8 +900,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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 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=
@@ -912,8 +912,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 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=
@@ -951,8 +951,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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.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=
@@ -962,8 +962,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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
-golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
+golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
 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=
@@ -975,8 +975,9 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 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/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 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=
@@ -991,8 +992,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
 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.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
-golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
 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=

From f7125ab61aaa02fd4c7ab0062a2dc9a57726e2ec Mon Sep 17 00:00:00 2001
From: Henrique Pimentel <66185935+HenriquerPimentel@users.noreply.github.com>
Date: Thu, 6 Jun 2024 09:06:59 +0100
Subject: [PATCH 105/131] Add `MAX_ROWS` option for CSV rendering (#30268)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This solution implements a new config variable MAX_ROWS, which
corresponds to the “Maximum allowed rows to render CSV files. (0 for no
limit)” and rewrites the Render function for CSV files in markup module.
Now the render function only reads the file once, having MAX_FILE_SIZE+1
as a reader limit and MAX_ROWS as a row limit. When the file is larger
than MAX_FILE_SIZE or has more rows than MAX_ROWS, it only renders until
the limit, and displays a user-friendly warning informing that the
rendered data is not complete, in the user's language.

---

Previously, when a CSV file was larger than the limit, the render
function lost its function to render the code. There were also multiple
reads to the file, in order to determine its size and render or
pre-render.

The warning: ![image](https://s3.amazonaws.com/i.snag.gy/vcKh90.jpg)
---
 custom/conf/app.example.ini    |  3 ++
 modules/markup/csv/csv.go      | 94 +++++++++++++---------------------
 modules/markup/csv/csv_test.go | 10 ----
 modules/setting/ui.go          |  3 ++
 4 files changed, 41 insertions(+), 69 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 7677168d83..e619aae729 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1334,6 +1334,9 @@ LEVEL = Info
 ;;
 ;; Maximum allowed file size in bytes to render CSV files as table. (Set to 0 for no limit).
 ;MAX_FILE_SIZE = 524288
+;;
+;; Maximum allowed rows to render CSV files. (Set to 0 for no limit)
+;MAX_ROWS = 2500
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 1dd26eb8ac..3d952b0de4 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -5,8 +5,6 @@ package markup
 
 import (
 	"bufio"
-	"bytes"
-	"fmt"
 	"html"
 	"io"
 	"regexp"
@@ -15,6 +13,8 @@ import (
 	"code.gitea.io/gitea/modules/csv"
 	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/translation"
+	"code.gitea.io/gitea/modules/util"
 )
 
 func init() {
@@ -81,86 +81,38 @@ func writeField(w io.Writer, element, class, field string) error {
 func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 	tmpBlock := bufio.NewWriter(output)
 	maxSize := setting.UI.CSV.MaxFileSize
+	maxRows := setting.UI.CSV.MaxRows
 
-	if maxSize == 0 {
-		return r.tableRender(ctx, input, tmpBlock)
+	if maxSize != 0 {
+		input = io.LimitReader(input, maxSize+1)
 	}
 
-	rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
-	if err != nil {
-		return err
-	}
-
-	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
-	}
-
-	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
-		}
-	}
-	if err = scan.Err(); err != nil {
-		return fmt.Errorf("fallbackRender scan: %w", 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
 	}
-
 	if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil {
 		return err
 	}
-	row := 1
+
+	row := 0
 	for {
 		fields, err := rd.Read()
-		if err == io.EOF {
+		if err == io.EOF || (row >= maxRows && maxRows != 0) {
 			break
 		}
 		if err != nil {
 			continue
 		}
+
 		if _, err := tmpBlock.WriteString("<tr>"); err != nil {
 			return err
 		}
 		element := "td"
-		if row == 1 {
+		if row == 0 {
 			element = "th"
 		}
-		if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row)); err != nil {
+		if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row+1)); err != nil {
 			return err
 		}
 		for _, field := range fields {
@@ -174,8 +126,32 @@ func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock
 
 		row++
 	}
+
 	if _, err = tmpBlock.WriteString("</table>"); err != nil {
 		return err
 	}
+
+	// Check if maxRows or maxSize is reached, and if true, warn.
+	if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) {
+		warn := `<table class="data-table"><tr><td>`
+		rawLink := ` <a href="` + ctx.Links.RawLink() + `/` + util.PathEscapeSegments(ctx.RelativePath) + `">`
+
+		// Try to get the user translation
+		if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
+			warn += locale.TrString("repo.file_too_large")
+			rawLink += locale.TrString("repo.file_view_raw")
+		} else {
+			warn += "The file is too large to be shown."
+			rawLink += "View Raw"
+		}
+
+		warn += rawLink + `</a></td></tr></table>`
+
+		// Write the HTML string to the output
+		if _, err := tmpBlock.WriteString(warn); err != nil {
+			return err
+		}
+	}
+
 	return tmpBlock.Flush()
 }
diff --git a/modules/markup/csv/csv_test.go b/modules/markup/csv/csv_test.go
index 3d12be477c..8c07184b21 100644
--- a/modules/markup/csv/csv_test.go
+++ b/modules/markup/csv/csv_test.go
@@ -4,8 +4,6 @@
 package markup
 
 import (
-	"bufio"
-	"bytes"
 	"strings"
 	"testing"
 
@@ -31,12 +29,4 @@ 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())
-	})
 }
diff --git a/modules/setting/ui.go b/modules/setting/ui.go
index 93855bca07..a8dc11d097 100644
--- a/modules/setting/ui.go
+++ b/modules/setting/ui.go
@@ -52,6 +52,7 @@ var UI = struct {
 
 	CSV struct {
 		MaxFileSize int64
+		MaxRows     int
 	} `ini:"ui.csv"`
 
 	Admin struct {
@@ -107,8 +108,10 @@ var UI = struct {
 	},
 	CSV: struct {
 		MaxFileSize int64
+		MaxRows     int
 	}{
 		MaxFileSize: 524288,
+		MaxRows:     2500,
 	},
 	Admin: struct {
 		UserPagingNum   int

From da4bbc42477ba04d175cc0775a0c5ec90c4c24fe Mon Sep 17 00:00:00 2001
From: Max Wipfli <mail@maxwipfli.ch>
Date: Thu, 6 Jun 2024 10:35:04 +0200
Subject: [PATCH 106/131] Allow including `Reviewed-on`/`Reviewed-by` lines for
 custom merge messages (#31211)

This PR introduces the `ReviewedOn` and `ReviewedBy` variables for the
default merge message templates (e.g.,
`.gitea/default_merge_message/MERGE_TEMPLATE.md`).

This allows customizing the default merge messages while retaining these
trailers.

This also moves the associated logic out of `pull.tmpl` into the
relevant Go function.

This is a first contribution towards #11077.

---

For illustration, this allows to recreate the "default default" merge
message with the following template:
```
.gitea/default_merge_message/MERGE_TEMPLATE.md
Merge pull request '${PullRequestTitle}' (${PullRequestReference}) from ${HeadBranch} into ${BaseBranch}

${ReviewedOn}
${ReviewedBy}
```
---
 .../usage/merge-message-templates.en-us.md     |  2 ++
 services/pull/merge.go                         | 18 ++++++++++++++----
 templates/repo/issue/view_content/pull.tmpl    |  6 ++----
 3 files changed, 18 insertions(+), 8 deletions(-)

diff --git a/docs/content/usage/merge-message-templates.en-us.md b/docs/content/usage/merge-message-templates.en-us.md
index fbdbd136f8..5116be3387 100644
--- a/docs/content/usage/merge-message-templates.en-us.md
+++ b/docs/content/usage/merge-message-templates.en-us.md
@@ -44,6 +44,8 @@ You can use the following variables enclosed in `${}` inside these templates whi
 - PullRequestIndex: Pull request's index number
 - PullRequestReference: Pull request's reference char with index number. i.e. #1, !2
 - ClosingIssues: return a string contains all issues which will be closed by this pull request i.e. `close #1, close #2`
+- ReviewedOn: Which pull request this commit belongs to. For example `Reviewed-on: https://gitea.com/foo/bar/pulls/1`
+- ReviewedBy: Who approved the pull request before the merge. For example `Reviewed-by: Jane Doe <jane.doe@example.com>`
 
 ## Rebase
 
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 20be7c5b5a..6b5e9ea330 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -46,6 +46,9 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
 	if err := pr.Issue.LoadPoster(ctx); err != nil {
 		return "", "", err
 	}
+	if err := pr.Issue.LoadRepo(ctx); err != nil {
+		return "", "", err
+	}
 
 	isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
 	issueReference := "#"
@@ -53,6 +56,9 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
 		issueReference = "!"
 	}
 
+	reviewedOn := fmt.Sprintf("Reviewed-on: %s/%s", setting.AppURL, pr.Issue.Link())
+	reviewedBy := pr.GetApprovers(ctx)
+
 	if mergeStyle != "" {
 		templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle)))
 		commit, err := baseGitRepo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
@@ -77,6 +83,8 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
 				"PullRequestPosterName":  pr.Issue.Poster.Name,
 				"PullRequestIndex":       strconv.FormatInt(pr.Index, 10),
 				"PullRequestReference":   fmt.Sprintf("%s%d", issueReference, pr.Index),
+				"ReviewedOn":             reviewedOn,
+				"ReviewedBy":             reviewedBy,
 			}
 			if pr.HeadRepo != nil {
 				vars["HeadRepoOwnerName"] = pr.HeadRepo.OwnerName
@@ -116,20 +124,22 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
 		return "", "", nil
 	}
 
+	body = fmt.Sprintf("%s\n%s", reviewedOn, reviewedBy)
+
 	// Squash merge has a different from other styles.
 	if mergeStyle == repo_model.MergeStyleSquash {
-		return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), "", nil
+		return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), body, nil
 	}
 
 	if pr.BaseRepoID == pr.HeadRepoID {
-		return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), "", nil
+		return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil
 	}
 
 	if pr.HeadRepo == nil {
-		return fmt.Sprintf("Merge pull request '%s' (%s%d) from <deleted>:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), "", nil
+		return fmt.Sprintf("Merge pull request '%s' (%s%d) from <deleted>:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil
 	}
 
-	return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), "", nil
+	return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), body, nil
 }
 
 func expandDefaultMergeMessage(template string, vars map[string]string) (message, body string) {
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 77378ef1bd..69e74da3a0 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -199,7 +199,6 @@
 
 				{{if .AllowMerge}} {{/* user is allowed to merge */}}
 					{{$prUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypePullRequests}}
-					{{$approvers := (.Issue.PullRequest.GetApprovers ctx)}}
 					{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash $prUnit.PullRequestsConfig.AllowFastForwardOnly}}
 						{{$hasPendingPullRequestMergeTip := ""}}
 						{{if .HasPendingPullRequestMerge}}
@@ -208,11 +207,10 @@
 						{{end}}
 						<div class="divider"></div>
 						<script type="module">
-							const issueUrl = window.location.origin + {{$.Issue.Link}};
 							const defaultMergeTitle = {{.DefaultMergeMessage}};
 							const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
-							const defaultMergeMessage = {{if .DefaultMergeBody}}{{.DefaultMergeBody}}{{else}}`Reviewed-on: ${issueUrl}\n` + {{$approvers}}{{end}};
-							const defaultSquashMergeMessage = {{if .DefaultSquashMergeBody}}{{.DefaultSquashMergeBody}}{{else}}`Reviewed-on: ${issueUrl}\n` + {{$approvers}}{{end}};
+							const defaultMergeMessage = {{.DefaultMergeBody}};
+							const defaultSquashMergeMessage = {{.DefaultSquashMergeBody}};
 							const mergeForm = {
 								'baseLink': {{.Link}},
 								'textCancel': {{ctx.Locale.Tr "cancel"}},

From 8e337467464ea1d3cedcdf50c9e8419e53add097 Mon Sep 17 00:00:00 2001
From: Kerwin Bryant <kerwin612@qq.com>
Date: Fri, 7 Jun 2024 07:22:03 +0800
Subject: [PATCH 107/131] Optimize repo-list layout to enhance visual
 experience (#31272)

before:

![1717655078227](https://github.com/go-gitea/gitea/assets/3371163/4d564f96-c2f8-46b1-996f-6cc7abb940ef)
***The problem was that the icon and text were not on a horizontal line,
and the horizontal was not centered;***

after:

![1717655094071](https://github.com/go-gitea/gitea/assets/3371163/b11797f6-05f8-486c-b5fd-df89d0cbdcfd)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 templates/user/settings/repos.tmpl | 12 ++++++------
 web_src/css/user.css               |  4 ----
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index 26b9dfeed9..a50fb586c7 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -84,17 +84,17 @@
 					<div class="ui middle aligned divided list">
 						{{range .Repos}}
 							<div class="item">
-								<div class="content">
+								<div class="content flex-text-block">
 									{{if .IsPrivate}}
-										{{svg "octicon-lock" 16 "tw-mr-1 iconFloat text gold"}}
+										{{svg "octicon-lock" 16 "text gold"}}
 									{{else if .IsFork}}
-										{{svg "octicon-repo-forked" 16 "tw-mr-1 iconFloat"}}
+										{{svg "octicon-repo-forked"}}
 									{{else if .IsMirror}}
-										{{svg "octicon-mirror" 16 "tw-mr-1 iconFloat"}}
+										{{svg "octicon-mirror"}}
 									{{else if .IsTemplate}}
-										{{svg "octicon-repo-template" 16 "tw-mr-1 iconFloat"}}
+										{{svg "octicon-repo-template"}}
 									{{else}}
-										{{svg "octicon-repo" 16 "tw-mr-1 iconFloat"}}
+										{{svg "octicon-repo"}}
 									{{end}}
 									<a class="name" href="{{.Link}}">{{.OwnerName}}/{{.Name}}</a>
 									<span>{{FileSize .Size}}</span>
diff --git a/web_src/css/user.css b/web_src/css/user.css
index af8a2f5adc..caabf1834c 100644
--- a/web_src/css/user.css
+++ b/web_src/css/user.css
@@ -77,10 +77,6 @@
   padding-bottom: 5px;
 }
 
-.user.settings .iconFloat {
-  float: left;
-}
-
 .user-orgs {
   display: flex;
   flex-flow: row wrap;

From ab1948d4a30aee919ba1ffc2402a2a9a7222e68c Mon Sep 17 00:00:00 2001
From: Kerwin Bryant <kerwin612@qq.com>
Date: Fri, 7 Jun 2024 07:49:53 +0800
Subject: [PATCH 108/131] fixed the dropdown menu for the top New button to
 expand to the left (#31273)

before:

![1717660314025](https://github.com/go-gitea/gitea/assets/3371163/17ae7a48-31c5-4c71-b285-f65d9106bf86)

after:

![1717660674763](https://github.com/go-gitea/gitea/assets/3371163/85f847ac-a044-4695-9004-26e6485288c6)
---
 templates/base/head_navbar.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 7a3e663c49..2b52247303 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -104,7 +104,7 @@
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 					<span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span>
 				</span>
-				<div class="menu">
+				<div class="menu left">
 					<a class="item" href="{{AppSubUrl}}/repo/create">
 						{{svg "octicon-plus"}} {{ctx.Locale.Tr "new_repo"}}
 					</a>

From 15debbbe4eb94c1855a0178e379b7e3d19bd07ad Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 7 Jun 2024 15:37:33 +0200
Subject: [PATCH 109/131] Enable poetry non-package mode (#31282)

[Poetry
1.8.0](https://github.com/python-poetry/poetry/releases/tag/1.8.0) added
support for [non-package
mode](https://python-poetry.org/docs/basic-usage/#operating-modes), e.g.
projects that are not python packages themselves like we are. Make use
of that and remove the previous workaround via `--no-root`.
---
 Makefile       | 4 ++--
 pyproject.toml | 5 +----
 2 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/Makefile b/Makefile
index d97360c9f4..b5a79091eb 100644
--- a/Makefile
+++ b/Makefile
@@ -878,7 +878,7 @@ node_modules: package-lock.json
 	@touch node_modules
 
 .venv: poetry.lock
-	poetry install --no-root
+	poetry install
 	@touch .venv
 
 .PHONY: update
@@ -895,7 +895,7 @@ update-js: node-check | node_modules
 update-py: node-check | node_modules
 	npx updates -u -f pyproject.toml
 	rm -rf .venv poetry.lock
-	poetry install --no-root
+	poetry install
 	@touch .venv
 
 .PHONY: fomantic
diff --git a/pyproject.toml b/pyproject.toml
index bb768d5cb1..0724a8e24a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,8 +1,5 @@
 [tool.poetry]
-name = "gitea"
-version = "0.0.0"
-description = ""
-authors = []
+package-mode = false
 
 [tool.poetry.dependencies]
 python = "^3.10"

From 291a00dc570a143092e5ad19cdad12939d3d70dc Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Fri, 7 Jun 2024 15:42:31 +0200
Subject: [PATCH 110/131] Fix and clean up `ConfirmModal` (#31283)

Bug: orange button color was removed in
https://github.com/go-gitea/gitea/pull/30475, replaced with red
Bug: translation text was not html-escaped
Refactor: Replaced as much jQuery as possible, added useful
`createElementFromHTML`
Refactor: Remove colors checks that don't exist on `.link-action`

<img width="381" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/5900bf6a-8a86-4a86-b368-0559cbfea66e">

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
---
 web_src/js/features/common-global.js     |  4 ++--
 web_src/js/features/comp/ConfirmModal.js | 25 ++++++++++++------------
 web_src/js/features/repo-issue-list.js   |  2 +-
 web_src/js/utils/dom.js                  |  7 +++++++
 web_src/js/utils/dom.test.js             |  5 +++++
 5 files changed, 28 insertions(+), 15 deletions(-)
 create mode 100644 web_src/js/utils/dom.test.js

diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 3b021d4485..65eb237dde 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -295,8 +295,8 @@ async function linkAction(e) {
     return;
   }
 
-  const isRisky = el.classList.contains('red') || el.classList.contains('yellow') || el.classList.contains('orange') || el.classList.contains('negative');
-  if (await confirmModal({content: modalConfirmContent, buttonColor: isRisky ? 'orange' : 'primary'})) {
+  const isRisky = el.classList.contains('red') || el.classList.contains('negative');
+  if (await confirmModal(modalConfirmContent, {confirmButtonColor: isRisky ? 'red' : 'primary'})) {
     await doRequest();
   }
 }
diff --git a/web_src/js/features/comp/ConfirmModal.js b/web_src/js/features/comp/ConfirmModal.js
index e64996a352..f9ad5c39cc 100644
--- a/web_src/js/features/comp/ConfirmModal.js
+++ b/web_src/js/features/comp/ConfirmModal.js
@@ -1,22 +1,23 @@
 import $ from 'jquery';
 import {svg} from '../../svg.js';
 import {htmlEscape} from 'escape-goat';
+import {createElementFromHTML} from '../../utils/dom.js';
 
 const {i18n} = window.config;
 
-export async function confirmModal(opts = {content: '', buttonColor: 'primary'}) {
+export function confirmModal(content, {confirmButtonColor = 'primary'} = {}) {
   return new Promise((resolve) => {
-    const $modal = $(`
-<div class="ui g-modal-confirm modal">
-  <div class="content">${htmlEscape(opts.content)}</div>
-  <div class="actions">
-    <button class="ui cancel button">${svg('octicon-x')} ${i18n.modal_cancel}</button>
-    <button class="ui ${opts.buttonColor || 'primary'} ok button">${svg('octicon-check')} ${i18n.modal_confirm}</button>
-  </div>
-</div>
-`);
-
-    $modal.appendTo(document.body);
+    const modal = createElementFromHTML(`
+      <div class="ui g-modal-confirm modal">
+        <div class="content">${htmlEscape(content)}</div>
+        <div class="actions">
+          <button class="ui cancel button">${svg('octicon-x')} ${htmlEscape(i18n.modal_cancel)}</button>
+          <button class="ui ${confirmButtonColor} ok button">${svg('octicon-check')} ${htmlEscape(i18n.modal_confirm)}</button>
+        </div>
+      </div>
+    `);
+    document.body.append(modal);
+    const $modal = $(modal);
     $modal.modal({
       onApprove() {
         resolve(true);
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 92f058c4d2..5d18a7ff8d 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -76,7 +76,7 @@ function initRepoIssueListCheckboxes() {
     // for delete
     if (action === 'delete') {
       const confirmText = e.target.getAttribute('data-action-delete-confirm');
-      if (!await confirmModal({content: confirmText, buttonColor: 'orange'})) {
+      if (!await confirmModal(confirmText, {confirmButtonColor: 'red'})) {
         return;
       }
     }
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index a48510b191..7289f19cbf 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -297,3 +297,10 @@ export function replaceTextareaSelection(textarea, text) {
     textarea.dispatchEvent(new CustomEvent('change', {bubbles: true, cancelable: true}));
   }
 }
+
+// Warning: Do not enter any unsanitized variables here
+export function createElementFromHTML(htmlString) {
+  const div = document.createElement('div');
+  div.innerHTML = htmlString.trim();
+  return div.firstChild;
+}
diff --git a/web_src/js/utils/dom.test.js b/web_src/js/utils/dom.test.js
new file mode 100644
index 0000000000..fd7d97cad5
--- /dev/null
+++ b/web_src/js/utils/dom.test.js
@@ -0,0 +1,5 @@
+import {createElementFromHTML} from './dom.js';
+
+test('createElementFromHTML', () => {
+  expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
+});

From 0188d82e4908eb173f7203d577f801f3168ffcb8 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 7 Jun 2024 23:15:17 +0800
Subject: [PATCH 111/131] Fix some URLs whose sub-path is missing (#31289)

Fix #31285
---
 templates/admin/packages/list.tmpl        | 2 +-
 templates/devtest/fetch-action.tmpl       | 2 +-
 templates/user/settings/applications.tmpl | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 863f11da25..d1d77b6220 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -5,7 +5,7 @@
 			{{ctx.Locale.Tr "admin.packages.total_size" (FileSize .TotalBlobSize)}},
 			{{ctx.Locale.Tr "admin.packages.unreferenced_size" (FileSize .TotalUnreferencedBlobSize)}})
 			<div class="ui right">
-				<form method="post" action="/admin/packages/cleanup">
+				<form method="post" action="{{AppSubUrl}}/admin/packages/cleanup">
 					{{.CsrfTokenHtml}}
 					<button class="ui primary tiny button">{{ctx.Locale.Tr "admin.packages.cleanup"}}</button>
 				</form>
diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl
index 2b25e6c9c4..66f41fc6de 100644
--- a/templates/devtest/fetch-action.tmpl
+++ b/templates/devtest/fetch-action.tmpl
@@ -25,7 +25,7 @@
 				<div><label><input name="check" type="checkbox"> check</label></div>
 				<div><button name="btn">submit post</button></div>
 			</form>
-			<form method="post" action="/no-such-uri" class="form-fetch-action">
+			<form method="post" action="no-such-uri" class="form-fetch-action">
 				<div class="tw-py-8">bad action url</div>
 				<div><button name="btn">submit test</button></div>
 			</form>
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 8c67653e58..3c1934dd8b 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" (`href="/api/swagger" target="_blank"`|SafeHTML) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i>
+						<i>{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i>
 					</p>
 					<div class="scoped-access-token-mount">
 						<scoped-access-token-selector

From 6106a61eff305f0271dba54e7535fcccf14a42e0 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 9 Jun 2024 16:29:29 +0800
Subject: [PATCH 112/131] Remove sub-path from container registry realm
 (#31293)

Container registry requires that the "/v2" must be in the root, so the
sub-path in AppURL should be removed
---
 modules/setting/packages.go                      |  5 -----
 modules/test/utils.go                            |  6 ++++--
 routers/api/packages/container/container.go      |  6 +++---
 routers/web/user/package.go                      |  8 +++++++-
 tests/integration/api_packages_container_test.go | 12 +++++++++---
 5 files changed, 23 insertions(+), 14 deletions(-)

diff --git a/modules/setting/packages.go b/modules/setting/packages.go
index b225615a24..00fba67b39 100644
--- a/modules/setting/packages.go
+++ b/modules/setting/packages.go
@@ -6,7 +6,6 @@ package setting
 import (
 	"fmt"
 	"math"
-	"net/url"
 	"os"
 	"path/filepath"
 
@@ -19,7 +18,6 @@ var (
 		Storage           *Storage
 		Enabled           bool
 		ChunkedUploadPath string
-		RegistryHost      string
 
 		LimitTotalOwnerCount int64
 		LimitTotalOwnerSize  int64
@@ -66,9 +64,6 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
 		return err
 	}
 
-	appURL, _ := url.Parse(AppURL)
-	Packages.RegistryHost = appURL.Host
-
 	Packages.ChunkedUploadPath = filepath.ToSlash(sec.Key("CHUNKED_UPLOAD_PATH").MustString("tmp/package-upload"))
 	if !filepath.IsAbs(Packages.ChunkedUploadPath) {
 		Packages.ChunkedUploadPath = filepath.ToSlash(filepath.Join(AppDataPath, Packages.ChunkedUploadPath))
diff --git a/modules/test/utils.go b/modules/test/utils.go
index 4a0c2f1b3b..8dee92fbce 100644
--- a/modules/test/utils.go
+++ b/modules/test/utils.go
@@ -34,8 +34,10 @@ func IsNormalPageCompleted(s string) bool {
 	return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
 }
 
-func MockVariableValue[T any](p *T, v T) (reset func()) {
+func MockVariableValue[T any](p *T, v ...T) (reset func()) {
 	old := *p
-	*p = v
+	if len(v) > 0 {
+		*p = v[0]
+	}
 	return func() { *p = old }
 }
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 2a6d44ba08..b0c4458d51 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -116,9 +116,9 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
 }
 
 func apiUnauthorizedError(ctx *context.Context) {
-	// TODO: it doesn't seem quite right but it doesn't really cause problem at the moment.
-	// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed, ideally.
-	ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`)
+	// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
+	realmURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), setting.AppSubURL+"/") + "/v2/token"
+	ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
 	apiErrorDefined(ctx, errUnauthorized)
 }
 
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 2a18796687..dad4c8f602 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -5,6 +5,7 @@ package user
 
 import (
 	"net/http"
+	"net/url"
 
 	"code.gitea.io/gitea/models/db"
 	org_model "code.gitea.io/gitea/models/organization"
@@ -15,6 +16,7 @@ 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/httplib"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
@@ -178,7 +180,11 @@ func ViewPackageVersion(ctx *context.Context) {
 
 	switch pd.Package.Type {
 	case packages_model.TypeContainer:
-		ctx.Data["RegistryHost"] = setting.Packages.RegistryHost
+		registryAppURL, err := url.Parse(httplib.GuessCurrentAppURL(ctx))
+		if err != nil {
+			registryAppURL, _ = url.Parse(setting.AppURL)
+		}
+		ctx.Data["RegistryHost"] = registryAppURL.Host
 	case packages_model.TypeAlpine:
 		branches := make(container.Set[string])
 		repositories := make(container.Set[string])
diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go
index 9ac6e5256b..fcd1cc529f 100644
--- a/tests/integration/api_packages_container_test.go
+++ b/tests/integration/api_packages_container_test.go
@@ -84,7 +84,7 @@ func TestPackageContainer(t *testing.T) {
 			Token string `json:"token"`
 		}
 
-		authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
+		defaultAuthenticateValues := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
 
 		t.Run("Anonymous", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
@@ -92,7 +92,7 @@ func TestPackageContainer(t *testing.T) {
 			req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
 			resp := MakeRequest(t, req, http.StatusUnauthorized)
 
-			assert.ElementsMatch(t, authenticate, resp.Header().Values("WWW-Authenticate"))
+			assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
 
 			req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
 			resp = MakeRequest(t, req, http.StatusOK)
@@ -115,6 +115,12 @@ func TestPackageContainer(t *testing.T) {
 
 			req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
 			MakeRequest(t, req, http.StatusUnauthorized)
+
+			defer test.MockVariableValue(&setting.AppURL, "https://domain:8443/sub-path/")()
+			defer test.MockVariableValue(&setting.AppSubURL, "/sub-path")()
+			req = NewRequest(t, "GET", "/v2")
+			resp = MakeRequest(t, req, http.StatusUnauthorized)
+			assert.Equal(t, `Bearer realm="https://domain:8443/v2/token",service="container_registry",scope="*"`, resp.Header().Get("WWW-Authenticate"))
 		})
 
 		t.Run("User", func(t *testing.T) {
@@ -123,7 +129,7 @@ func TestPackageContainer(t *testing.T) {
 			req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
 			resp := MakeRequest(t, req, http.StatusUnauthorized)
 
-			assert.ElementsMatch(t, authenticate, resp.Header().Values("WWW-Authenticate"))
+			assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
 
 			req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)).
 				AddBasicAuth(user.Name)

From 4f7d6feab7e6cb6e8c5914a5b6cd20a64fd49c29 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Mon, 10 Jun 2024 00:27:20 +0000
Subject: [PATCH 113/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_fr-FR.ini | 110 ++++++++++++++++++++++++++++++++
 1 file changed, 110 insertions(+)

diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 9a1a756264..230107fc96 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -25,6 +25,7 @@ enable_javascript=Ce site Web nécessite JavaScript.
 toc=Sommaire
 licenses=Licences
 return_to_gitea=Revenir à Gitea
+more_items=Plus d'éléments
 
 username=Nom d'utilisateur
 email=Courriel
@@ -113,6 +114,7 @@ loading=Chargement…
 error=Erreur
 error404=La page que vous essayez d'atteindre <strong>n'existe pas</strong> ou <strong>vous n'êtes pas autorisé</strong> à la voir.
 go_back=Retour
+invalid_data=Données invalides : %v
 
 never=Jamais
 unknown=Inconnu
@@ -143,17 +145,43 @@ name=Nom
 value=Valeur
 
 filter=Filtrer
+filter.clear=Effacer le filtre
 filter.is_archived=Archivé
+filter.not_archived=Non archivé
+filter.is_fork=Bifurqué
+filter.not_fork=Non bifurqué
+filter.is_mirror=Miroité
+filter.not_mirror=Non miroité
 filter.is_template=Modèle
+filter.not_template=Pas un modèle
 filter.public=Public
 filter.private=Privé
 
+no_results_found=Aucun résultat trouvé.
 
 [search]
+search=Rechercher…
+type_tooltip=Type de recherche
+fuzzy=Approximative
+fuzzy_tooltip=Inclure également les résultats proches de la recherche
 exact=Exact
 exact_tooltip=Inclure uniquement les résultats qui correspondent exactement au terme de recherche
+repo_kind=Chercher des dépôts…
+user_kind=Chercher des utilisateurs…
+org_kind=Chercher des organisations…
+team_kind=Chercher des équipes…
+code_kind=Chercher du code…
+code_search_unavailable=La recherche dans le code n’est pas disponible actuellement. Veuillez contacter l’administrateur de votre instance Gitea.
+code_search_by_git_grep=Les résultats de recherche de code actuels sont fournis par « git grep ». L’administrateur peut activer l’indexeur de dépôt, qui pourrait fournir de meilleurs résultats.
+package_kind=Chercher des paquets…
+project_kind=Chercher des projets…
+branch_kind=Chercher des branches…
+commit_kind=Chercher des révisions…
+runner_kind=Chercher des exécuteurs…
+no_results=Aucun résultat correspondant trouvé.
 issue_kind=Recherche de tickets…
 pull_kind=Recherche de demandes d’ajouts…
+keyword_search_unavailable=La recherche par mot clé n’est pas disponible actuellement. Veuillez contacter l’administrateur de votre instance Gitea.
 
 [aria]
 navbar=Barre de navigation
@@ -260,6 +288,7 @@ email_title=Paramètres de Messagerie
 smtp_addr=Hôte SMTP
 smtp_port=Port SMTP
 smtp_from=Envoyer les courriels en tant que
+smtp_from_invalid=L’adresse « Envoyer le courriel sous » est invalide
 smtp_from_helper=Adresse courriel utilisée par Gitea. Utilisez directement votre adresse ou la forme « Nom <email@example.com> ».
 mailer_user=Utilisateur SMTP
 mailer_password=Mot de passe SMTP
@@ -319,6 +348,7 @@ env_config_keys=Configuration de l'environnement
 env_config_keys_prompt=Les variables d'environnement suivantes seront également ajoutées à votre fichier de configuration :
 
 [home]
+nav_menu=Menu de navigation
 uname_holder=Nom d’utilisateur ou adresse courriel
 password_holder=Mot de passe
 switch_dashboard_context=Basculer le contexte du tableau de bord
@@ -367,6 +397,7 @@ 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_ex=Un nouveau courriel de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans la prochaine %s pour terminer le processus d’inscription. Si votre adresse courriel est incorrecte, vous pouvez vous reconnecter et la modifier.
 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.
@@ -376,6 +407,7 @@ prohibit_login=Connexion interdite
 prohibit_login_desc=Votre compte n'autorise pas la connexion, veuillez contacter l'administrateur de votre site.
 resent_limit_prompt=Désolé, vous avez récemment demandé un courriel d'activation. Veuillez réessayer dans 3 minutes.
 has_unconfirmed_mail=Bonjour %s, votre adresse courriel (<b>%s</b>) n’a pas été confirmée. Si vous n’avez reçu aucun mail de confirmation ou souhaitez renouveler l’envoi, cliquez sur le bouton ci-dessous.
+change_unconfirmed_mail_address=Si votre adresse courriel d’inscription est incorrecte, vous pouvez la modifier ici et renvoyer un nouvel courriel de confirmation.
 resend_mail=Cliquez ici pour renvoyer un mail de confirmation
 email_not_associate=L’adresse courriel n’est associée à aucun compte.
 send_reset_mail=Envoyer un courriel de récupération du compte
@@ -404,6 +436,7 @@ oauth_signin_submit=Lier un compte
 oauth.signin.error=Une erreur s'est produite lors du traitement de la demande d'autorisation. Si cette erreur persiste, veuillez contacter l'administrateur du site.
 oauth.signin.error.access_denied=La demande d'autorisation a été refusée.
 oauth.signin.error.temporarily_unavailable=L'autorisation a échoué car le serveur d'authentification est temporairement indisponible. Veuillez réessayer plus tard.
+oauth_callback_unable_auto_reg=L’inscription automatique est activée, mais le fournisseur OAuth2 %[1]s a signalé des champs manquants : %[2]s, impossible de créer un compte automatiquement, veuillez créer ou lier un compte, ou bien contacter l’administrateur du site.
 openid_connect_submit=Se connecter
 openid_connect_title=Se connecter à un compte existant
 openid_connect_desc=L'URI OpenID choisie est inconnue. Associez-le à un nouveau compte ici.
@@ -556,6 +589,7 @@ team_name_been_taken=Le nom d'équipe est déjà pris.
 team_no_units_error=Autoriser l’accès à au moins une section du dépôt.
 email_been_used=Cette adresse courriel est déjà utilisée.
 email_invalid=Cette adresse courriel est invalide.
+email_domain_is_not_allowed=Le domaine <b>%s</b> du courriel utilisateur entre en conflit avec EMAIL_DOMAIN_ALLOWLIST ou EMAIL_DOMAIN_BLOCKLIST. Veuillez vous assurer que votre opération est attendue.
 openid_been_used=Adresse OpenID "%s" déjà utilisée.
 username_password_incorrect=Identifiant ou mot de passe invalide.
 password_complexity=Le mot de passe ne respecte pas les exigences de complexité:
@@ -567,6 +601,8 @@ enterred_invalid_repo_name=Le nom de dépôt saisi est incorrect.
 enterred_invalid_org_name=Le nom de l'organisation que vous avez entré est incorrect.
 enterred_invalid_owner_name=Le nom du nouveau propriétaire est invalide.
 enterred_invalid_password=Le mot de passe saisi est incorrect.
+unset_password=L’utilisateur n’a pas défini de mot de passe.
+unsupported_login_type=Le type de connexion n’est pas pris en charge pour supprimer le compte.
 user_not_exist=Cet utilisateur n'existe pas.
 team_not_exist=L'équipe n'existe pas.
 last_org_owner=Vous ne pouvez pas retirer le dernier utilisateur de l’équipe « propriétaires ». Il doit y avoir au moins un propriétaire dans chaque organisation.
@@ -616,6 +652,29 @@ 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.
 
+block.block=Bloquer
+block.block.user=Bloquer l’utilisateur
+block.block.org=Bloquer l’utilisateur pour l’organisation
+block.block.failure=Impossible de bloquer l’utilisateur : %s
+block.unblock=Débloquer
+block.unblock.failure=Impossible de débloquer l’utilisateur : %s
+block.blocked=Vous avez bloqué cet utilisateur.
+block.title=Bloquer un utilisateur
+block.info=Bloquer un utilisateur l’empêche d’interagir avec des dépôts, comme ouvrir ou commenter des demandes de fusion ou des tickets. Apprenez-en plus sur le blocage d’un utilisateur.
+block.info_1=Bloquer un utilisateur empêche les actions suivantes sur votre compte et vos dépôts :
+block.info_2=suivre votre compte
+block.info_3=vous envoyer des notifications en vous @mentionnant
+block.info_4=vous inviter en tant que collaborateur de son(ses) dépôt(s)
+block.info_5=aimer, bifurquer ou suivre vos dépôts
+block.info_6=ouvrir ou commenter vos tickets et demandes d’ajouts
+block.info_7=réagir à vos commentaires dans les tickets ou les demandes d’ajout
+block.user_to_block=Utilisateur à bloquer
+block.note=Note
+block.note.title=Note facultative :
+block.note.info=La note n’est pas visible par l’utilisateur bloqué.
+block.note.edit=Modifier la note
+block.list=Utilisateurs bloqués
+block.list.none=Vous n’avez bloqué aucun utilisateur.
 
 [settings]
 profile=Profil
@@ -658,6 +717,7 @@ cancel=Annuler
 language=Langue
 ui=Thème
 hidden_comment_types=Catégories de commentaires masqués
+hidden_comment_types_description=Cochez les catégories suivantes pour masquer les commentaires correspondants des fils d'activité. Par exemple, « Label » cache les commentaires du genre « Cerise a attribué le label Bug il y a 2 heures. »
 hidden_comment_types.ref_tooltip=Commentaires où ce ticket a été référencé sur un autre ticket, révision, etc.
 hidden_comment_types.issue_ref_tooltip=Commentaires où l’utilisateur change la branche/étiquette associée au ticket
 comment_type_group_reference=Référence
@@ -704,6 +764,8 @@ manage_themes=Sélectionner le thème par défaut
 manage_openid=Gérer les adresses OpenID
 email_desc=Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et, à condition qu'elle ne soit pas cachée, les opérations Git basées sur le Web.
 theme_desc=Ce sera votre thème par défaut sur le site.
+theme_colorblindness_help=Support du thème daltonien
+theme_colorblindness_prompt=Gitea fournit depuis peu des thèmes daltonien basé sur un spectre coloré réduit. Encore en développement, de futures améliorations devraient enrichir les fichiers de thèmes CSS.
 primary=Principale
 activated=Activé
 requires_activation=Nécessite une activation
@@ -953,7 +1015,9 @@ fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être m
 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.
+fork.blocked_user=Impossible de bifurquer le dépôt car vous êtes bloqué par son propriétaire.
 use_template=Utiliser ce modèle
+open_with_editor=Ouvrir avec %s
 download_zip=Télécharger le ZIP
 download_tar=Télécharger le TAR.GZ
 download_bundle=Télécharger le BUNDLE
@@ -1006,6 +1070,7 @@ watchers=Observateurs
 stargazers=Fans
 stars_remove_warning=Ceci supprimera toutes les étoiles de ce dépôt.
 forks=Bifurcations
+stars=Favoris
 reactions_more=et %d de plus
 unit_disabled=L'administrateur du site a désactivé cette section du dépôt.
 language_other=Autre
@@ -1127,6 +1192,7 @@ watch=Suivre
 unstar=Retirer des favoris
 star=Ajouter aux favoris
 fork=Bifurcation
+action.blocked_user=Impossible d’effectuer cette action car vous êtes bloqué par le propriétaire du dépôt.
 download_archive=Télécharger ce dépôt
 more_operations=Plus d'opérations
 
@@ -1172,6 +1238,8 @@ file_view_rendered=Voir le rendu
 file_view_raw=Voir le Raw
 file_permalink=Lien permanent
 file_too_large=Le fichier est trop gros pour être affiché.
+code_preview_line_from_to=Lignes %[1]d à %[2]d dans %[3]s
+code_preview_line_in=Ligne %[1]d dans %[2]s
 invisible_runes_header=`Ce fichier contient des caractères Unicode invisibles.`
 invisible_runes_description=`Ce fichier contient des caractères Unicode invisibles à l'œil nu, mais peuvent être traités différemment par un ordinateur. Si vous pensez que c'est intentionnel, vous pouvez ignorer cet avertissement. Utilisez le bouton Échappe pour les dévoiler.`
 ambiguous_runes_header=`Ce fichier contient des caractères Unicode ambigus.`
@@ -1226,6 +1294,7 @@ editor.or=ou
 editor.cancel_lower=Annuler
 editor.commit_signed_changes=Réviser les changements (signé)
 editor.commit_changes=Réviser les changements
+editor.add_tmpl=Ajouter {filename}
 editor.add=Ajouter %s
 editor.update=Actualiser %s
 editor.delete=Supprimer %s
@@ -1253,6 +1322,8 @@ editor.file_editing_no_longer_exists=Impossible de modifier le fichier « %s 
 editor.file_deleting_no_longer_exists=Impossible de supprimer le fichier « %s » car il n’existe plus dans ce dépôt.
 editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. <a target="_blank" rel="noopener noreferrer" href="%s">Cliquez ici</a> pour voir les changements ou <strong>soumettez de nouveau</strong> pour les écraser.
 editor.file_already_exists=Un fichier nommé "%s" existe déjà dans ce dépôt.
+editor.commit_id_not_matching=L’ID de la révision ne correspond pas à l’ID lorsque vous avez commencé à éditer. Faites une révision dans une branche de correctif puis fusionnez.
+editor.push_out_of_date=Cet envoi semble être obsolète.
 editor.commit_empty_file_header=Réviser un fichier vide
 editor.commit_empty_file_text=Le fichier que vous allez réviser est vide. Continuer ?
 editor.no_changes_to_show=Il n’y a aucune modification à afficher.
@@ -1277,6 +1348,7 @@ 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.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.search_branch=Cette branche
 commits.search_all=Toutes les branches
 commits.author=Auteur
 commits.message=Message
@@ -1306,6 +1378,7 @@ commitstatus.success=Succès
 ext_issues=Accès aux tickets externes
 ext_issues.desc=Lien vers un gestionnaire de tickets externe.
 
+projects.desc=Gérer les tickets et les demandes d’ajouts dans les projets.
 projects.description=Description (facultative)
 projects.description_placeholder=Description
 projects.create=Créer un projet
@@ -1333,6 +1406,7 @@ 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.delete=Supprimer la colonne
+projects.column.deletion_desc=La suppression d’une colonne déplace tous ses tickets dans la colonne par défaut. Continuer ?
 projects.column.color=Couleur
 projects.open=Ouvrir
 projects.close=Fermer
@@ -1367,6 +1441,9 @@ issues.new.assignees=Assignés
 issues.new.clear_assignees=Supprimer les affectations
 issues.new.no_assignees=Sans assignation
 issues.new.no_reviewers=Sans évaluateur
+issues.new.blocked_user=Impossible de créer un ticket car vous êtes bloqué par le propriétaire du dépôt.
+issues.edit.already_changed=Impossible d’enregistrer le ticket. Il semble que le contenu ait été modifié par un autre utilisateur. Veuillez rafraîchir la page et réessayer pour éviter d’écraser ses modifications.
+issues.edit.blocked_user=Impossible de modifier ce contenu car vous êtes bloqué par son propriétaire.
 issues.choose.get_started=Démarrons
 issues.choose.open_external_link=Ouvrir
 issues.choose.blank=Par défaut
@@ -1477,8 +1554,11 @@ issues.no_content=Sans contenu.
 issues.close=Fermer le ticket
 issues.comment_pull_merged_at=a fusionné la révision %[1]s dans %[2]s %[3]s
 issues.comment_manually_pull_merged_at=a fusionné manuellement la révision %[1]s dans %[2]s %[3]s
+issues.close_comment_issue=Commenter et Fermer
 issues.reopen_issue=Rouvrir
+issues.reopen_comment_issue=Commenter et Réouvrir
 issues.create_comment=Commenter
+issues.comment.blocked_user=Impossible créer ou de modifier un commentaire car vous êtes bloqué par le propriétaire du dépôt.
 issues.closed_at=`a fermé ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.`
 issues.reopened_at=`a réouvert ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.`
 issues.commit_ref_at=`a référencé ce ticket depuis une révision <a id="%[1]s" href="#%[1]s"> %[2]s</a>.`
@@ -1677,6 +1757,8 @@ compare.compare_head=comparer
 
 pulls.desc=Active les demandes d’ajouts et l’évaluation du code.
 pulls.new=Nouvelle demande d'ajout
+pulls.new.blocked_user=Impossible de créer une demande d’ajout car vous êtes bloqué par le propriétaire du dépôt.
+pulls.edit.already_changed=Impossible d’enregistrer la demande d’ajout. Il semble que le contenu ait été modifié par un autre utilisateur. Veuillez rafraîchir la page et réessayer afin d’éviter d’écraser leurs modifications.
 pulls.view=Voir la demande d'ajout
 pulls.compare_changes=Nouvelle demande d’ajout
 pulls.allow_edits_from_maintainers=Autoriser les modifications des mainteneurs
@@ -1822,6 +1904,7 @@ pulls.recently_pushed_new_branches=Vous avez soumis sur la branche <strong>%[1]s
 
 pull.deleted_branch=(supprimé) : %s
 
+comments.edit.already_changed=Impossible d’enregistrer ce commentaire. Il semble que le contenu ait été modifié par un autre utilisateur. Veuillez rafraîchir la page et réessayer afin d’éviter d’écraser leurs modifications.
 
 milestones.new=Nouveau jalon
 milestones.closed=%s fermé
@@ -1898,7 +1981,10 @@ 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.pulse=Impulsion
+activity.navbar.code_frequency=Fréquence du code
 activity.navbar.contributors=Contributeurs
+activity.navbar.recent_commits=Révisions récentes
 activity.period.filter_label=Période :
 activity.period.daily=1 jour
 activity.period.halfweekly=3 jours
@@ -2017,7 +2103,9 @@ settings.branches.add_new_rule=Ajouter une nouvelle règle
 settings.advanced_settings=Paramètres avancés
 settings.wiki_desc=Activer le wiki du dépôt
 settings.use_internal_wiki=Utiliser le wiki interne
+settings.default_wiki_branch_name=Nom de la branche du Wiki par défaut
 settings.default_wiki_everyone_access=Autorisation d’accès par défaut pour les utilisateurs connectés :
+settings.failed_to_change_default_wiki_branch=Impossible de modifier la branche du wiki par défaut.
 settings.use_external_wiki=Utiliser un wiki externe
 settings.external_wiki_url=URL Wiki externe
 settings.external_wiki_url_error=L’URL du wiki externe n’est pas une URL valide.
@@ -2048,6 +2136,9 @@ 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_desc=Mode Projets (type de projets à afficher)
+settings.projects_mode_repo=Projets de dépôt uniquement
+settings.projects_mode_owner=Projets d’utilisateur ou d’organisation uniquement
 settings.projects_mode_all=Tous les projets
 settings.actions_desc=Activer les actions du dépôt
 settings.admin_settings=Paramètres administrateur
@@ -2074,6 +2165,7 @@ settings.convert_fork_succeed=La bifurcation a été convertie en dépôt standa
 settings.transfer=Changer de propriétaire
 settings.transfer.rejected=Le transfert du dépôt a été rejeté.
 settings.transfer.success=Le transfert du dépôt a réussi.
+settings.transfer.blocked_user=Impossible de transférer ce dépôt car vous êtes bloqué par l’acquéreur.
 settings.transfer_abort=Annuler le transfert
 settings.transfer_abort_invalid=Vous ne pouvez pas annuler un transfert de dépôt inexistant.
 settings.transfer_abort_success=Le transfert du dépôt vers %s a bien été stoppé.
@@ -2119,6 +2211,7 @@ settings.add_collaborator_success=Le collaborateur a été ajouté.
 settings.add_collaborator_inactive_user=Impossible d'ajouter un utilisateur inactif en tant que collaborateur.
 settings.add_collaborator_owner=Impossible d'ajouter un propriétaire en tant que collaborateur.
 settings.add_collaborator_duplicate=Le collaborateur est déjà ajouté à ce dépôt.
+settings.add_collaborator.blocked_user=Ce collaborateur est bloqué par le propriétaire du dépôt ou inversement.
 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 ?
@@ -2557,13 +2650,16 @@ find_file.no_matching=Aucun fichier correspondant trouvé
 error.csv.too_large=Impossible de visualiser le fichier car il est trop volumineux.
 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.
+error.broken_git_hook=Les crochets Git de ce dépôt semblent cassés. Veuillez suivre la <a target="_blank" rel="noreferrer" href="%s">documentation</a> pour les corriger, puis pousser des révisions pour actualiser le statut.
 
 [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.
+code_frequency.what=fréquence du code
 contributors.what=contributions
+recent_commits.what=révisions récentes
 
 [org]
 org_name_holder=Nom de l'organisation
@@ -2677,6 +2773,7 @@ teams.add_nonexistent_repo=Le dépôt que vous essayez d'ajouter n'existe pas, v
 teams.add_duplicate_users=L’utilisateur est déjà un membre de l’équipe.
 teams.repos.none=Aucun dépôt n'est accessible par cette équipe.
 teams.members.none=Aucun membre dans cette équipe.
+teams.members.blocked_user=Impossible d’ajouter l’utilisateur car il est bloqué par l’organisation.
 teams.specific_repositories=Dépôts spécifiques
 teams.specific_repositories_helper=Les membres auront seulement accès aux dépôts explicitement ajoutés à l'équipe. Sélectionner ceci <strong>ne supprimera pas automatiquement</strong> les dépôts déjà ajoutés avec <i>Tous les dépôts</i>.
 teams.all_repositories=Tous les dépôts
@@ -2689,6 +2786,7 @@ teams.invite.by=Invité par %s
 teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe.
 
 [admin]
+maintenance=Maintenance
 dashboard=Tableau de bord
 self_check=Autodiagnostique
 identity_access=Identité et accès
@@ -2712,6 +2810,7 @@ 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.maintenance_operations=Opérations de maintenance
 dashboard.system_status=État du système
 dashboard.operation_name=Nom de l'Opération
 dashboard.operation_switch=Basculer
@@ -2997,11 +3096,14 @@ auths.tips=Conseils
 auths.tips.oauth2.general=Authentification OAuth2
 auths.tips.oauth2.general.tip=Lors de l'enregistrement d'une nouvelle authentification OAuth2, l'URL de rappel/redirection doit être :
 auths.tip.oauth2_provider=Fournisseur OAuth2
+auths.tip.bitbucket=Créez un nouveau jeton OAuth sur https://bitbucket.org/account/user/{your username}/oauth-consumers/new et ajoutez la permission “Compte” - “Lecture”.
 auths.tip.nextcloud=`Enregistrez un nouveau consommateur OAuth sur votre instance en utilisant le menu "Paramètres -> Sécurité -> Client OAuth 2.0"`
 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_new=Enregistrez 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écouverte OpenID « https://{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
 auths.tip.discord=Enregistrer une nouvelle application sur https://discordapp.com/developers/applications/me
 auths.tip.gitea=Enregistrez une nouvelle application OAuth2. Le guide peut être trouvé sur https://docs.gitea.com/development/oauth2-provider
@@ -3135,6 +3237,7 @@ config.picture_config=Configuration de l'avatar
 config.picture_service=Service d'Imagerie
 config.disable_gravatar=Désactiver Gravatar
 config.enable_federated_avatar=Activer les avatars unifiés
+config.open_with_editor_app_help=Les éditeurs disponibles via « Ouvrir avec ». Si laissé vide, la valeur par défaut sera utilisée. Développez pour voir la valeur par défaut.
 
 config.git_config=Configuration de Git
 config.git_disable_diff_highlight=Désactiver la surbrillance syntaxique de Diff
@@ -3214,11 +3317,13 @@ 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.startup_warnings=Avertissements au démarrage :
 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 … ».
+self_check.location_origin_mismatch=L’URL actuelle (%[1]s) ne correspond pas à l’URL vue par Gitea (%[2]). Si vous utilisez un proxy inverse, assurez-vous que les en-têtes « Host » et « X-Forwarded-Proto » sont correctement définis.
 
 [action]
 create_repo=a créé le dépôt <a href="%s">%s</a>
@@ -3246,6 +3351,7 @@ mirror_sync_create=a synchronisé la nouvelle référence <a href="%[2]s">%[3]s<
 mirror_sync_delete=a synchronisé puis supprimé la nouvelle référence <code>%[2]s</code> vers <a href="%[1]s">%[3]s</a> depuis le miroir
 approve_pull_request=`a approuvé <a href="%[1]s">%[3]s#%[2]s</a>`
 reject_pull_request=`a suggérés des changements pour <a href="%[1]s">%[3]s#%[2]s</a>`
+publish_release=`a publié <a href="%[2]s"> "%[4]s" </a> dans <a href="%[1]s">%[3]s</a>`
 review_dismissed=`a révoqué l’évaluation de <b>%[4]s</b> dans <a href="%[1]s">%[3]s#%[2]s</a>`
 review_dismissed_reason=Raison :
 create_branch=a créé la branche <a href="%[2]s">%[3]s</a> dans <a href="%[1]s">%[4]s</a>
@@ -3312,6 +3418,7 @@ error.unit_not_allowed=Vous n'êtes pas autorisé à accéder à cette section d
 title=Paquets
 desc=Gérer les paquets du dépôt.
 empty=Il n'y pas de paquet pour le moment.
+no_metadata=Pas de métadonnées.
 empty.documentation=Pour plus d'informations sur le registre de paquets, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
 empty.repo=Avez-vous téléchargé un paquet, mais il n'est pas affiché ici? Allez dans les <a href="%[1]s">paramètres du paquet</a> et liez le à ce dépôt.
 registry.documentation=Pour plus d’informations sur le registre %s, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
@@ -3393,6 +3500,7 @@ npm.install=Pour installer le paquet en utilisant npm, exécutez la commande sui
 npm.install2=ou ajoutez-le au fichier package.json :
 npm.dependencies=Dépendances
 npm.dependencies.development=Dépendances de développement
+npm.dependencies.bundle=Dépendances emballées
 npm.dependencies.peer=Dépendances de pairs
 npm.dependencies.optional=Dépendances optionnelles
 npm.details.tag=Balise
@@ -3532,6 +3640,8 @@ runs.scheduled=Planifié
 runs.pushed_by=soumis par
 runs.invalid_workflow_helper=La configuration du flux de travail est invalide. Veuillez vérifier votre fichier %s.
 runs.no_matching_online_runner_helper=Aucun exécuteur en ligne correspondant au libellé %s
+runs.no_job_without_needs=Le flux de travail doit contenir au moins une tâche sans dépendance.
+runs.no_job=Le flux de travail doit contenir au moins une tâche
 runs.actor=Acteur
 runs.status=Statut
 runs.actors_no_select=Tous les acteurs

From a2304cb163ce5e097078e71f49d4d5cb4c8b20d9 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 10 Jun 2024 12:12:31 +0200
Subject: [PATCH 114/131] Remove jQuery `.text()` (#30506)

Remove and forbid [.text()](https://api.jquery.com/text/). Tested some,
but not all functionality, but I think these are pretty safe
replacements.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 .eslintrc.yaml                             |   4 +-
 templates/repo/editor/commit_form.tmpl     |   6 +-
 templates/repo/settings/collaboration.tmpl |   8 +-
 web_src/js/features/common-global.js       |  95 ++++++++++--------
 web_src/js/features/imagediff.js           |  10 +-
 web_src/js/features/notification.js        |  14 +--
 web_src/js/features/repo-editor.js         | 111 ++++++++-------------
 web_src/js/features/repo-issue-edit.js     |   7 +-
 web_src/js/features/repo-issue.js          |  22 ++--
 web_src/js/features/repo-legacy.js         |   4 +-
 web_src/js/features/repo-settings.js       |  45 ++++-----
 web_src/js/index.js                        |   4 +-
 12 files changed, 161 insertions(+), 169 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index 0eda8a1877..cbfe0220e8 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -324,7 +324,7 @@ rules:
   jquery/no-sizzle: [2]
   jquery/no-slide: [2]
   jquery/no-submit: [2]
-  jquery/no-text: [0]
+  jquery/no-text: [2]
   jquery/no-toggle: [2]
   jquery/no-trigger: [0]
   jquery/no-trim: [2]
@@ -477,7 +477,7 @@ rules:
   no-jquery/no-slide: [2]
   no-jquery/no-sub: [2]
   no-jquery/no-support: [2]
-  no-jquery/no-text: [0]
+  no-jquery/no-text: [2]
   no-jquery/no-trigger: [0]
   no-jquery/no-trim: [2]
   no-jquery/no-type: [2]
diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl
index 21ef63288f..61122417d2 100644
--- a/templates/repo/editor/commit_form.tmpl
+++ b/templates/repo/editor/commit_form.tmpl
@@ -23,7 +23,7 @@
 		<div class="quick-pull-choice js-quick-pull-choice">
 			<div class="field">
 				<div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}">
-					<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}}>
+					<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-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}}
@@ -43,9 +43,9 @@
 				<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}}>
+							<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-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}}>
+							<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-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"}}
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index ed4d5e7eb3..255d0d59a1 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -19,13 +19,13 @@
 						<div class="flex-item-trailing">
 							<div class="flex-text-block">
 								{{svg "octicon-shield-lock"}}
-								<div class="ui inline dropdown access-mode" data-url="{{$.Link}}/access_mode" data-uid="{{.ID}}" data-last-value="{{printf "%d" .Collaboration.Mode}}">
+								<div class="ui dropdown custom access-mode" data-url="{{$.Link}}/access_mode" data-uid="{{.ID}}" data-last-value="{{.Collaboration.Mode}}">
 									<div class="text">{{if eq .Collaboration.Mode 1}}{{ctx.Locale.Tr "repo.settings.collaboration.read"}}{{else if eq .Collaboration.Mode 2}}{{ctx.Locale.Tr "repo.settings.collaboration.write"}}{{else if eq .Collaboration.Mode 3}}{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}{{else}}{{ctx.Locale.Tr "repo.settings.collaboration.undefined"}}{{end}}</div>
 									{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 									<div class="menu">
-										<div class="item" data-text="{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}" data-value="3">{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}</div>
-										<div class="item" data-text="{{ctx.Locale.Tr "repo.settings.collaboration.write"}}" data-value="2">{{ctx.Locale.Tr "repo.settings.collaboration.write"}}</div>
-										<div class="item" data-text="{{ctx.Locale.Tr "repo.settings.collaboration.read"}}" data-value="1">{{ctx.Locale.Tr "repo.settings.collaboration.read"}}</div>
+										<div class="item" data-value="3">{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}</div>
+										<div class="item" data-value="2">{{ctx.Locale.Tr "repo.settings.collaboration.write"}}</div>
+										<div class="item" data-value="1">{{ctx.Locale.Tr "repo.settings.collaboration.read"}}</div>
 									</div>
 								</div>
 							</div>
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 65eb237dde..5162c71509 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -301,52 +301,65 @@ async function linkAction(e) {
   }
 }
 
-export function initGlobalLinkActions() {
-  function showDeletePopup(e) {
-    e.preventDefault();
-    const $this = $(this);
-    const dataArray = $this.data();
-    let filter = '';
-    if (this.getAttribute('data-modal-id')) {
-      filter += `#${this.getAttribute('data-modal-id')}`;
-    }
+export function initGlobalDeleteButton() {
+  // ".delete-button" shows a confirmation modal defined by `data-modal-id` attribute.
+  // Some model/form elements will be filled by `data-id` / `data-name` / `data-data-xxx` attributes.
+  // If there is a form defined by `data-form`, then the form will be submitted as-is (without any modification).
+  // If there is no form, then the data will be posted to `data-url`.
+  // TODO: it's not encouraged to use this method. `show-modal` does far better than this.
+  for (const btn of document.querySelectorAll('.delete-button')) {
+    btn.addEventListener('click', (e) => {
+      e.preventDefault();
 
-    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);
+      // eslint-disable-next-line github/no-dataset -- code depends on the camel-casing
+      const dataObj = btn.dataset;
+
+      const modalId = btn.getAttribute('data-modal-id');
+      const modal = document.querySelector(`.delete.modal${modalId ? `#${modalId}` : ''}`);
+
+      // set the modal "display name" by `data-name`
+      const modalNameEl = modal.querySelector('.name');
+      if (modalNameEl) modalNameEl.textContent = btn.getAttribute('data-name');
+
+      // fill the modal elements with data-xxx attributes: `data-data-organization-name="..."` => `<span class="dataOrganizationName">...</span>`
+      for (const [key, value] of Object.entries(dataObj)) {
+        if (key.startsWith('data')) {
+          const textEl = modal.querySelector(`.${key}`);
+          if (textEl) textEl.textContent = value;
+        }
       }
-    }
 
-    $dialog.modal({
-      closable: false,
-      onApprove: async () => {
-        if ($this.data('type') === 'form') {
-          $($this.data('form')).trigger('submit');
-          return;
-        }
-        const postData = new FormData();
-        for (const [key, value] of Object.entries(dataArray)) {
-          if (key && key.startsWith('data')) {
-            postData.append(key.slice(4), value);
+      $(modal).modal({
+        closable: false,
+        onApprove: async () => {
+          // if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."`
+          if (btn.getAttribute('data-type') === 'form') {
+            const formSelector = btn.getAttribute('data-form');
+            const form = document.querySelector(formSelector);
+            if (!form) throw new Error(`no form named ${formSelector} found`);
+            form.submit();
           }
-          if (key === 'id') {
-            postData.append('id', value);
-          }
-        }
 
-        const response = await POST($this.data('url'), {data: postData});
-        if (response.ok) {
-          const data = await response.json();
-          window.location.href = data.redirect;
-        }
-      },
-    }).modal('show');
+          // prepare an AJAX form by data attributes
+          const postData = new FormData();
+          for (const [key, value] of Object.entries(dataObj)) {
+            if (key.startsWith('data')) { // for data-data-xxx (HTML) -> dataXxx (form)
+              postData.append(key.slice(4), value);
+            }
+            if (key === 'id') { // for data-id="..."
+              postData.append('id', value);
+            }
+          }
+
+          const response = await POST(btn.getAttribute('data-url'), {data: postData});
+          if (response.ok) {
+            const data = await response.json();
+            window.location.href = data.redirect;
+          }
+        },
+      }).modal('show');
+    });
   }
-
-  // Helpers.
-  $('.delete-button').on('click', showDeletePopup);
 }
 
 function initGlobalShowModal() {
@@ -382,7 +395,7 @@ function initGlobalShowModal() {
       } 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
+        $attrTarget[0].textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
       }
     }
 
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index d1b139ffde..2d28b4b526 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -79,20 +79,20 @@ 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: this.querySelector('.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: this.querySelector('.bounds-info-before'),
     }];
 
     await Promise.all(imageInfos.map(async (info) => {
       const [success] = await Promise.all(Array.from(info.$images, (img) => {
         return loadElem(img, info.path);
       }));
-      // only the first images is associated with $boundsInfo
-      if (!success) info.$boundsInfo.text('(image error)');
+      // only the first images is associated with boundsInfo
+      if (!success && info.boundsInfo) info.boundsInfo.textContent = '(image error)';
       if (info.mime === 'image/svg+xml') {
         const resp = await GET(info.path);
         const text = await resp.text();
@@ -102,7 +102,7 @@ export function initImageDiff() {
             this.setAttribute('width', bounds.width);
             this.setAttribute('height', bounds.height);
           });
-          hideElem(info.$boundsInfo);
+          hideElem(info.boundsInfo);
         }
       }
     }));
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 8e5a1f83db..f045879dec 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -47,17 +47,13 @@ async function receiveUpdateCount(event) {
 }
 
 export function initNotificationCount() {
-  const $notificationCount = $('.notification_count');
-
-  if (!$notificationCount.length) {
-    return;
-  }
+  if (!document.querySelector('.notification_count')) return;
 
   let usingPeriodicPoller = false;
   const startPeriodicPoller = (timeout, lastCount) => {
     if (timeout <= 0 || !Number.isFinite(timeout)) return;
     usingPeriodicPoller = true;
-    lastCount = lastCount ?? $notificationCount.text();
+    lastCount = lastCount ?? getCurrentCount();
     setTimeout(async () => {
       await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
     }, timeout);
@@ -121,8 +117,12 @@ export function initNotificationCount() {
   startPeriodicPoller(notificationSettings.MinTimeout);
 }
 
+function getCurrentCount() {
+  return document.querySelector('.notification_count').textContent;
+}
+
 async function updateNotificationCountWithCallback(callback, timeout, lastCount) {
-  const currentCount = $('.notification_count').text();
+  const currentCount = getCurrentCount();
   if (lastCount !== currentCount) {
     callback(notificationSettings.MinTimeout, currentCount);
     return;
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index a5232cb4b6..b4fae4f6aa 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -1,7 +1,7 @@
 import $ from 'jquery';
 import {htmlEscape} from 'escape-goat';
 import {createCodeEditor} from './codeeditor.js';
-import {hideElem, showElem} from '../utils/dom.js';
+import {hideElem, queryElems, showElem} from '../utils/dom.js';
 import {initMarkupContent} from '../markup/content.js';
 import {attachRefIssueContextPopup} from './contextpopup.js';
 import {POST} from '../modules/fetch.js';
@@ -40,98 +40,75 @@ function initEditPreviewTab($form) {
   }
 }
 
-function initEditorForm() {
-  const $form = $('.repository .edit.form');
-  if (!$form) return;
-  initEditPreviewTab($form);
-}
-
-function getCursorPosition($e) {
-  const el = $e.get(0);
-  let pos = 0;
-  if ('selectionStart' in el) {
-    pos = el.selectionStart;
-  } else if ('selection' in document) {
-    el.focus();
-    const Sel = document.selection.createRange();
-    const SelLength = document.selection.createRange().text.length;
-    Sel.moveStart('character', -el.value.length);
-    pos = Sel.text.length - SelLength;
-  }
-  return pos;
-}
-
 export function initRepoEditor() {
-  initEditorForm();
+  const $editArea = $('.repository.editor textarea#edit_area');
+  if (!$editArea.length) return;
 
-  $('.js-quick-pull-choice-option').on('change', function () {
-    if ($(this).val() === 'commit-to-new-branch') {
-      showElem('.quick-pull-branch-name');
-      document.querySelector('.quick-pull-branch-name input').required = true;
-    } else {
-      hideElem('.quick-pull-branch-name');
-      document.querySelector('.quick-pull-branch-name input').required = false;
-    }
-    $('#commit-button').text(this.getAttribute('button_text'));
-  });
-
-  const joinTreePath = ($fileNameEl) => {
-    const parts = [];
-    $('.breadcrumb span.section').each(function () {
-      const $element = $(this);
-      if ($element.find('a').length) {
-        parts.push($element.find('a').text());
+  for (const el of queryElems('.js-quick-pull-choice-option')) {
+    el.addEventListener('input', () => {
+      if (el.value === 'commit-to-new-branch') {
+        showElem('.quick-pull-branch-name');
+        document.querySelector('.quick-pull-branch-name input').required = true;
       } else {
-        parts.push($element.text());
+        hideElem('.quick-pull-branch-name');
+        document.querySelector('.quick-pull-branch-name input').required = false;
       }
+      document.querySelector('#commit-button').textContent = el.getAttribute('data-button-text');
     });
-    if ($fileNameEl.val()) parts.push($fileNameEl.val());
-    $('#tree_path').val(parts.join('/'));
-  };
-
-  const $editFilename = $('#file-name');
-  $editFilename.on('input', function () {
-    const parts = $(this).val().split('/');
+  }
 
+  const filenameInput = document.querySelector('#file-name');
+  function joinTreePath() {
+    const parts = [];
+    for (const el of document.querySelectorAll('.breadcrumb span.section')) {
+      const link = el.querySelector('a');
+      parts.push(link ? link.textContent : el.textContent);
+    }
+    if (filenameInput.value) {
+      parts.push(filenameInput.value);
+    }
+    document.querySelector('#tree_path').value = parts.join('/');
+  }
+  filenameInput.addEventListener('input', function () {
+    const parts = filenameInput.value.split('/');
     if (parts.length > 1) {
       for (let i = 0; i < parts.length; ++i) {
         const value = parts[i];
         if (i < parts.length - 1) {
           if (value.length) {
-            $(`<span class="section"><a href="#">${htmlEscape(value)}</a></span>`).insertBefore($(this));
-            $('<div class="breadcrumb-divider">/</div>').insertBefore($(this));
+            $(`<span class="section"><a href="#">${htmlEscape(value)}</a></span>`).insertBefore($(filenameInput));
+            $('<div class="breadcrumb-divider">/</div>').insertBefore($(filenameInput));
           }
         } else {
-          $(this).val(value);
+          filenameInput.value = value;
         }
         this.setSelectionRange(0, 0);
       }
     }
-
-    joinTreePath($(this));
+    joinTreePath();
   });
-
-  $editFilename.on('keydown', function (e) {
-    const $section = $('.breadcrumb span.section');
-
+  filenameInput.addEventListener('keydown', function (e) {
+    const sections = queryElems('.breadcrumb span.section');
+    const dividers = queryElems('.breadcrumb .breadcrumb-divider');
     // Jump back to last directory once the filename is empty
-    if (e.code === 'Backspace' && getCursorPosition($(this)) === 0 && $section.length > 0) {
+    if (e.code === 'Backspace' && filenameInput.selectionStart === 0 && sections.length > 0) {
       e.preventDefault();
-      const $divider = $('.breadcrumb .breadcrumb-divider');
-      const value = $section.last().find('a').text();
-      $(this).val(value + $(this).val());
+      const lastSection = sections[sections.length - 1];
+      const lastDivider = dividers.length ? dividers[dividers.length - 1] : null;
+      const value = lastSection.querySelector('a').textContent;
+      filenameInput.value = value + filenameInput.value;
       this.setSelectionRange(value.length, value.length);
-      $section.last().remove();
-      $divider.last().remove();
-      joinTreePath($(this));
+      lastDivider?.remove();
+      lastSection.remove();
+      joinTreePath();
     }
   });
 
-  const $editArea = $('.repository.editor textarea#edit_area');
-  if (!$editArea.length) return;
+  const $form = $('.repository.editor .edit.form');
+  initEditPreviewTab($form);
 
   (async () => {
-    const editor = await createCodeEditor($editArea[0], $editFilename[0]);
+    const editor = await createCodeEditor($editArea[0], filenameInput);
 
     // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
     // to enable or disable the commit button
diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js
index 9a8d737e01..29b96f5127 100644
--- a/web_src/js/features/repo-issue-edit.js
+++ b/web_src/js/features/repo-issue-edit.js
@@ -189,11 +189,12 @@ export function initRepoIssueCommentEdit() {
   // 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 target = this.getAttribute('data-target');
+    const quote = document.querySelector(`#${target}`).textContent.replace(/\n/g, '\n> ');
     const content = `> ${quote}\n\n`;
+
     let editor;
-    if ($(this).hasClass('quote-reply-diff')) {
+    if (this.classList.contains('quote-reply-diff')) {
       const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
       editor = await handleReply($replyBtn);
     } else {
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 95910e34bc..3cbbdc41fc 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -278,11 +278,12 @@ export function initRepoPullRequestUpdate() {
 
   $('.update-button > .dropdown').dropdown({
     onChange(_text, _value, $choice) {
-      const url = $choice[0].getAttribute('data-do');
+      const choiceEl = $choice[0];
+      const url = choiceEl.getAttribute('data-do');
       if (url) {
         const buttonText = pullUpdateButton.querySelector('.button-text');
         if (buttonText) {
-          buttonText.textContent = $choice.text();
+          buttonText.textContent = choiceEl.textContent;
         }
         pullUpdateButton.setAttribute('data-do', url);
       }
@@ -567,14 +568,15 @@ export function initRepoPullRequestReview() {
 export function initRepoIssueReferenceIssue() {
   // Reference issue
   $(document).on('click', '.reference-issue', function (event) {
-    const $this = $(this);
-    const content = $(`#${$this.data('target')}`).text();
-    const poster = $this.data('poster-username');
-    const reference = toAbsoluteUrl($this.data('reference'));
-    const $modal = $($this.data('modal'));
-    $modal.find('textarea[name="content"]').val(`${content}\n\n_Originally posted by @${poster} in ${reference}_`);
-    $modal.modal('show');
-
+    const target = this.getAttribute('data-target');
+    const content = document.querySelector(`#${target}`)?.textContent ?? '';
+    const poster = this.getAttribute('data-poster-username');
+    const reference = toAbsoluteUrl(this.getAttribute('data-reference'));
+    const modalSelector = this.getAttribute('data-modal');
+    const modal = document.querySelector(modalSelector);
+    const textarea = modal.querySelector('textarea[name="content"]');
+    textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
+    $(modal).modal('show');
     event.preventDefault();
   });
 }
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 2323d818c2..e53d86cca0 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -272,9 +272,9 @@ export function initRepoCommentForm() {
       }
 
       $list.find('.selected').html(`
-        <a class="item muted sidebar-item-link" href=${$(this).data('href')}>
+        <a class="item muted sidebar-item-link" href=${htmlEscape(this.getAttribute('href'))}>
           ${icon}
-          ${htmlEscape($(this).text())}
+          ${htmlEscape(this.textContent)}
         </a>
       `);
 
diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 52c5de2bfa..652f8ac290 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -1,47 +1,46 @@
 import $ from 'jquery';
 import {minimatch} from 'minimatch';
 import {createMonaco} from './codeeditor.js';
-import {onInputDebounce, toggleElem} from '../utils/dom.js';
+import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.js';
 import {POST} from '../modules/fetch.js';
 
 const {appSubUrl, csrfToken} = window.config;
 
 export function initRepoSettingsCollaboration() {
   // Change collaborator access mode
-  $('.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 = el.getAttribute('data-last-value');
+  for (const dropdownEl of queryElems('.page-content.repository .ui.dropdown.access-mode')) {
+    const textEl = dropdownEl.querySelector(':scope > .text');
+    $(dropdownEl).dropdown({
+      async action(text, value) {
+        dropdownEl.classList.add('is-loading', 'loading-icon-2px');
+        const lastValue = dropdownEl.getAttribute('data-last-value');
+        $(dropdownEl).dropdown('hide');
         try {
-          el.setAttribute('data-last-value', value);
-          $dropdown.dropdown('hide');
-          const data = new FormData();
-          data.append('uid', el.getAttribute('data-uid'));
-          data.append('mode', value);
-          await POST(el.getAttribute('data-url'), {data});
+          const uid = dropdownEl.getAttribute('data-uid');
+          await POST(dropdownEl.getAttribute('data-url'), {data: new URLSearchParams({uid, 'mode': value})});
+          textEl.textContent = text;
+          dropdownEl.setAttribute('data-last-value', value);
         } catch {
-          $text.text('(error)'); // prevent from misleading users when error occurs
-          el.setAttribute('data-last-value', lastValue);
+          textEl.textContent = '(error)'; // prevent from misleading users when error occurs
+          dropdownEl.setAttribute('data-last-value', lastValue);
+        } finally {
+          dropdownEl.classList.remove('is-loading');
         }
       },
-      onChange(_value, text, _$choice) {
-        $text.text(text); // update the text when using keyboard navigating
-      },
       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
+        // 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', el.getAttribute('data-last-value'));
+          const $item = $(dropdownEl).dropdown('get item', dropdownEl.getAttribute('data-last-value'));
           if ($item) {
-            $dropdown.dropdown('set selected', el.getAttribute('data-last-value'));
+            $(dropdownEl).dropdown('set selected', dropdownEl.getAttribute('data-last-value'));
           } else {
-            $text.text('(none)'); // prevent from misleading users when the access mode is undefined
+            textEl.textContent = '(none)'; // prevent from misleading users when the access mode is undefined
           }
         }, 0);
       },
     });
-  });
+  }
 }
 
 export function initRepoSettingSearchTeamBox() {
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 1867556eee..12cd0ee15a 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -43,7 +43,7 @@ import {
   initGlobalDropzone,
   initGlobalEnterQuickSubmit,
   initGlobalFormDirtyLeaveConfirm,
-  initGlobalLinkActions,
+  initGlobalDeleteButton,
   initHeadNavbarContentToggle,
 } from './features/common-global.js';
 import {initRepoTopicBar} from './features/repo-home.js';
@@ -103,7 +103,7 @@ onDomReady(() => {
   initGlobalDropzone();
   initGlobalEnterQuickSubmit();
   initGlobalFormDirtyLeaveConfirm();
-  initGlobalLinkActions();
+  initGlobalDeleteButton();
 
   initCommonOrganization();
   initCommonIssueListQuickGoto();

From 507fbf4c3ceffba9143edbe421a134b904210a4c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 10 Jun 2024 22:49:33 +0200
Subject: [PATCH 115/131] Use `querySelector` over alternative DOM methods
 (#31280)

As per
https://github.com/go-gitea/gitea/pull/30115#discussion_r1626060164,
prefer `querySelector` by enabling
[`unicorn/prefer-query-selector`](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-query-selector.md)
and autofixing all except 10 issues.

According to
[this](https://old.reddit.com/r/learnjavascript/comments/i0f5o8/performance_of_getelementbyid_vs_queryselector/),
querySelector may be faster as well, so it's a win-win.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 .eslintrc.yaml                                |  2 +-
 web_src/js/components/DashboardRepoList.vue   |  4 +-
 web_src/js/components/DiffCommitSelector.vue  |  2 +-
 web_src/js/components/DiffFileList.vue        |  4 +-
 web_src/js/components/DiffFileTree.vue        |  2 +-
 web_src/js/components/RepoActionView.vue      |  2 +-
 .../js/components/RepoActivityTopAuthors.vue  |  2 +-
 .../js/components/RepoBranchTagSelector.vue   |  2 +-
 .../components/ScopedAccessTokenSelector.vue  | 12 ++--
 web_src/js/features/admin/common.js           | 60 +++++++++----------
 web_src/js/features/citation.js               |  8 +--
 web_src/js/features/code-frequency.js         |  2 +-
 web_src/js/features/colorpicker.js            |  2 +-
 web_src/js/features/common-global.js          |  4 +-
 web_src/js/features/common-issue-list.js      |  2 +-
 web_src/js/features/comp/SearchUserBox.js     |  2 +-
 web_src/js/features/comp/WebHookEditor.js     |  6 +-
 web_src/js/features/contributors.js           |  2 +-
 web_src/js/features/copycontent.js            |  2 +-
 web_src/js/features/heatmap.js                |  2 +-
 web_src/js/features/install.js                | 16 ++---
 web_src/js/features/notification.js           |  8 +--
 web_src/js/features/pull-view-file.js         |  4 +-
 web_src/js/features/recent-commits.js         |  2 +-
 web_src/js/features/repo-diff-commitselect.js |  2 +-
 web_src/js/features/repo-diff-filetree.js     |  4 +-
 web_src/js/features/repo-diff.js              |  2 +-
 web_src/js/features/repo-editor.js            |  2 +-
 web_src/js/features/repo-findfile.js          |  4 +-
 web_src/js/features/repo-graph.js             | 28 ++++-----
 web_src/js/features/repo-home.js              |  8 +--
 web_src/js/features/repo-issue-edit.js        |  6 +-
 web_src/js/features/repo-issue-list.js        |  4 +-
 web_src/js/features/repo-issue-pr-form.js     |  2 +-
 web_src/js/features/repo-issue.js             | 14 ++---
 web_src/js/features/repo-migrate.js           |  8 +--
 web_src/js/features/repo-migration.js         | 22 +++----
 web_src/js/features/repo-projects.js          | 14 ++---
 web_src/js/features/repo-release.js           |  6 +-
 web_src/js/features/repo-settings.js          | 16 ++---
 web_src/js/features/sshkey-helper.js          |  4 +-
 web_src/js/features/user-auth-webauthn.js     |  6 +-
 web_src/js/features/user-auth.js              |  4 +-
 web_src/js/features/user-settings.js          |  6 +-
 web_src/js/markup/anchors.js                  |  9 +--
 web_src/js/standalone/devtest.js              |  6 +-
 web_src/js/standalone/swagger.js              |  2 +-
 47 files changed, 165 insertions(+), 168 deletions(-)

diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index cbfe0220e8..3b25995c09 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -798,7 +798,7 @@ rules:
   unicorn/prefer-object-has-own: [0]
   unicorn/prefer-optional-catch-binding: [2]
   unicorn/prefer-prototype-methods: [0]
-  unicorn/prefer-query-selector: [0]
+  unicorn/prefer-query-selector: [2]
   unicorn/prefer-reflect-apply: [0]
   unicorn/prefer-regexp-test: [2]
   unicorn/prefer-set-has: [0]
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 3f9f427cd7..23984b3164 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -101,7 +101,7 @@ const sfc = {
   },
 
   mounted() {
-    const el = document.getElementById('dashboard-repo-list');
+    const el = document.querySelector('#dashboard-repo-list');
     this.changeReposFilter(this.reposFilter);
     $(el).find('.dropdown').dropdown();
     nextTick(() => {
@@ -330,7 +330,7 @@ const sfc = {
 };
 
 export function initDashboardRepoList() {
-  const el = document.getElementById('dashboard-repo-list');
+  const el = document.querySelector('#dashboard-repo-list');
   if (el) {
     createApp(sfc).mount(el);
   }
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index 352d085731..c28be67e38 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -5,7 +5,7 @@ import {GET} from '../modules/fetch.js';
 export default {
   components: {SvgIcon},
   data: () => {
-    const el = document.getElementById('diff-commit-select');
+    const el = document.querySelector('#diff-commit-select');
     return {
       menuVisible: false,
       isLoading: false,
diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue
index 916780d913..806c8385bb 100644
--- a/web_src/js/components/DiffFileList.vue
+++ b/web_src/js/components/DiffFileList.vue
@@ -7,10 +7,10 @@ export default {
     return {store: diffTreeStore()};
   },
   mounted() {
-    document.getElementById('show-file-list-btn').addEventListener('click', this.toggleFileList);
+    document.querySelector('#show-file-list-btn').addEventListener('click', this.toggleFileList);
   },
   unmounted() {
-    document.getElementById('show-file-list-btn').removeEventListener('click', this.toggleFileList);
+    document.querySelector('#show-file-list-btn').removeEventListener('click', this.toggleFileList);
   },
   methods: {
     toggleFileList() {
diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue
index cddfee1e04..fd5120f18b 100644
--- a/web_src/js/components/DiffFileTree.vue
+++ b/web_src/js/components/DiffFileTree.vue
@@ -112,7 +112,7 @@ export default {
     updateState(visible) {
       const btn = document.querySelector('.diff-toggle-file-tree-button');
       const [toShow, toHide] = btn.querySelectorAll('.icon');
-      const tree = document.getElementById('diff-file-tree');
+      const tree = document.querySelector('#diff-file-tree');
       const newTooltip = btn.getAttribute(visible ? 'data-hide-text' : 'data-show-text');
       btn.setAttribute('data-tooltip-content', newTooltip);
       toggleElem(tree, visible);
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 8b39d0504b..7f6524c7e3 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -325,7 +325,7 @@ const sfc = {
 export default sfc;
 
 export function initRepositoryActionView() {
-  const el = document.getElementById('repo-action-view');
+  const el = document.querySelector('#repo-action-view');
   if (!el) return;
 
   // TODO: the parent element's full height doesn't work well now,
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index a41fb61d78..295641f7e5 100644
--- a/web_src/js/components/RepoActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -51,7 +51,7 @@ const sfc = {
 };
 
 export function initRepoActivityTopAuthorsChart() {
-  const el = document.getElementById('repo-activity-top-authors-chart');
+  const el = document.querySelector('#repo-activity-top-authors-chart');
   if (el) {
     createApp(sfc).mount(el);
   }
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index 87530225e3..d18378bea1 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -85,7 +85,7 @@ const sfc = {
         this.isViewBranch = false;
         this.$refs.dropdownRefName.textContent = item.name;
         if (this.setAction) {
-          document.getElementById(this.branchForm)?.setAttribute('action', url);
+          document.querySelector(`#${this.branchForm}`)?.setAttribute('action', url);
         } else {
           $(`#${this.branchForm} input[name="refURL"]`).val(url);
         }
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
index 103cc525ad..9ff3627c11 100644
--- a/web_src/js/components/ScopedAccessTokenSelector.vue
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -43,25 +43,25 @@ const sfc = {
   },
 
   mounted() {
-    document.getElementById('scoped-access-submit').addEventListener('click', this.onClickSubmit);
+    document.querySelector('#scoped-access-submit').addEventListener('click', this.onClickSubmit);
   },
 
   unmounted() {
-    document.getElementById('scoped-access-submit').removeEventListener('click', this.onClickSubmit);
+    document.querySelector('#scoped-access-submit').removeEventListener('click', this.onClickSubmit);
   },
 
   methods: {
     onClickSubmit(e) {
       e.preventDefault();
 
-      const warningEl = document.getElementById('scoped-access-warning');
+      const warningEl = document.querySelector('#scoped-access-warning');
       // check that at least one scope has been selected
-      for (const el of document.getElementsByClassName('access-token-select')) {
+      for (const el of document.querySelectorAll('.access-token-select')) {
         if (el.value) {
           // Hide the error if it was visible from previous attempt.
           hideElem(warningEl);
           // Submit the form.
-          document.getElementById('scoped-access-form').submit();
+          document.querySelector('#scoped-access-form').submit();
           // Don't show the warning.
           return;
         }
@@ -78,7 +78,7 @@ export default sfc;
  * Initialize category toggle sections
  */
 export function initScopedAccessTokenCategories() {
-  for (const el of document.getElementsByClassName('scoped-access-token-mount')) {
+  for (const el of document.querySelectorAll('.scoped-access-token-mount')) {
     createApp({})
       .component('scoped-access-token-selector', sfc)
       .mount(el);
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 3c90b546b8..429d6a808c 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -6,7 +6,7 @@ import {POST} from '../../modules/fetch.js';
 const {appSubUrl} = window.config;
 
 function onSecurityProtocolChange() {
-  if (Number(document.getElementById('security_protocol')?.value) > 0) {
+  if (Number(document.querySelector('#security_protocol')?.value) > 0) {
     showElem('.has-tls');
   } else {
     hideElem('.has-tls');
@@ -21,34 +21,34 @@ export function initAdminCommon() {
 
   // New user
   if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) {
-    document.getElementById('login_type')?.addEventListener('change', function () {
+    document.querySelector('#login_type')?.addEventListener('change', function () {
       if (this.value?.substring(0, 1) === '0') {
-        document.getElementById('user_name')?.removeAttribute('disabled');
-        document.getElementById('login_name')?.removeAttribute('required');
+        document.querySelector('#user_name')?.removeAttribute('disabled');
+        document.querySelector('#login_name')?.removeAttribute('required');
         hideElem('.non-local');
         showElem('.local');
-        document.getElementById('user_name')?.focus();
+        document.querySelector('#user_name')?.focus();
 
         if (this.getAttribute('data-password') === 'required') {
-          document.getElementById('password')?.setAttribute('required', 'required');
+          document.querySelector('#password')?.setAttribute('required', 'required');
         }
       } else {
         if (document.querySelector('.admin.edit.user')) {
-          document.getElementById('user_name')?.setAttribute('disabled', 'disabled');
+          document.querySelector('#user_name')?.setAttribute('disabled', 'disabled');
         }
-        document.getElementById('login_name')?.setAttribute('required', 'required');
+        document.querySelector('#login_name')?.setAttribute('required', 'required');
         showElem('.non-local');
         hideElem('.local');
-        document.getElementById('login_name')?.focus();
+        document.querySelector('#login_name')?.focus();
 
-        document.getElementById('password')?.removeAttribute('required');
+        document.querySelector('#password')?.removeAttribute('required');
       }
     });
   }
 
   function onUsePagedSearchChange() {
     const searchPageSizeElements = document.querySelectorAll('.search-page-size');
-    if (document.getElementById('use_paged_search').checked) {
+    if (document.querySelector('#use_paged_search').checked) {
       showElem('.search-page-size');
       for (const el of searchPageSizeElements) {
         el.querySelector('input')?.setAttribute('required', 'required');
@@ -67,7 +67,7 @@ export function initAdminCommon() {
       input.removeAttribute('required');
     }
 
-    const provider = document.getElementById('oauth2_provider').value;
+    const provider = document.querySelector('#oauth2_provider').value;
     switch (provider) {
       case 'openidConnect':
         document.querySelector('.open_id_connect_auto_discovery_url input').setAttribute('required', 'required');
@@ -91,19 +91,19 @@ export function initAdminCommon() {
   }
 
   function onOAuth2UseCustomURLChange(applyDefaultValues) {
-    const provider = document.getElementById('oauth2_provider').value;
+    const provider = document.querySelector('#oauth2_provider').value;
     hideElem('.oauth2_use_custom_url_field');
     for (const input of document.querySelectorAll('.oauth2_use_custom_url_field input[required]')) {
       input.removeAttribute('required');
     }
 
     const elProviderCustomUrlSettings = document.querySelector(`#${provider}_customURLSettings`);
-    if (elProviderCustomUrlSettings && document.getElementById('oauth2_use_custom_url').checked) {
+    if (elProviderCustomUrlSettings && document.querySelector('#oauth2_use_custom_url').checked) {
       for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
         if (applyDefaultValues) {
-          document.getElementById(`oauth2_${custom}`).value = document.getElementById(`${provider}_${custom}`).value;
+          document.querySelector(`#oauth2_${custom}`).value = document.querySelector(`#${provider}_${custom}`).value;
         }
-        const customInput = document.getElementById(`${provider}_${custom}`);
+        const customInput = document.querySelector(`#${provider}_${custom}`);
         if (customInput && customInput.getAttribute('data-available') === 'true') {
           for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) {
             input.setAttribute('required', 'required');
@@ -115,12 +115,12 @@ export function initAdminCommon() {
   }
 
   function onEnableLdapGroupsChange() {
-    toggleElem(document.getElementById('ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
+    toggleElem(document.querySelector('#ldap-group-options'), $('.js-ldap-group-toggle')[0].checked);
   }
 
   // New authentication
   if (document.querySelector('.admin.new.authentication')) {
-    document.getElementById('auth_type')?.addEventListener('change', function () {
+    document.querySelector('#auth_type')?.addEventListener('change', function () {
       hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi');
 
       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]')) {
@@ -180,25 +180,25 @@ export function initAdminCommon() {
       }
     });
     $('#auth_type').trigger('change');
-    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));
+    document.querySelector('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
+    document.querySelector('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
+    document.querySelector('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+    document.querySelector('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true));
     $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
   }
   // Edit authentication
   if (document.querySelector('.admin.edit.authentication')) {
-    const authType = document.getElementById('auth_type')?.value;
+    const authType = document.querySelector('#auth_type')?.value;
     if (authType === '2' || authType === '5') {
-      document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange);
+      document.querySelector('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
       $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
       onEnableLdapGroupsChange();
       if (authType === '2') {
-        document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
+        document.querySelector('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
       }
     } else if (authType === '6') {
-      document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
-      document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
+      document.querySelector('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
+      document.querySelector('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
       onOAuth2Change(false);
     }
   }
@@ -206,13 +206,13 @@ export function initAdminCommon() {
   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.
-      document.getElementById('oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent(this.value)}/callback`;
+      document.querySelector('#oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent(this.value)}/callback`;
     }).trigger('input');
   }
 
   // Notice
   if (document.querySelector('.admin.notice')) {
-    const detailModal = document.getElementById('detail-modal');
+    const detailModal = document.querySelector('#detail-modal');
 
     // Attach view detail modals
     $('.view-detail').on('click', function () {
@@ -244,7 +244,7 @@ export function initAdminCommon() {
           break;
       }
     });
-    document.getElementById('delete-selection')?.addEventListener('click', async function (e) {
+    document.querySelector('#delete-selection')?.addEventListener('click', async function (e) {
       e.preventDefault();
       this.classList.add('is-loading', 'disabled');
       const data = new FormData();
diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js
index 918a467136..245ba56f81 100644
--- a/web_src/js/features/citation.js
+++ b/web_src/js/features/citation.js
@@ -27,9 +27,9 @@ export async function initCitationFileCopyContent() {
 
   if (!pageData.citationFileContent) return;
 
-  const citationCopyApa = document.getElementById('citation-copy-apa');
-  const citationCopyBibtex = document.getElementById('citation-copy-bibtex');
-  const inputContent = document.getElementById('citation-copy-content');
+  const citationCopyApa = document.querySelector('#citation-copy-apa');
+  const citationCopyBibtex = document.querySelector('#citation-copy-bibtex');
+  const inputContent = document.querySelector('#citation-copy-content');
 
   if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return;
 
@@ -41,7 +41,7 @@ export async function initCitationFileCopyContent() {
     citationCopyApa.classList.toggle('primary', !isBibtex);
   };
 
-  document.getElementById('cite-repo-button')?.addEventListener('click', async (e) => {
+  document.querySelector('#cite-repo-button')?.addEventListener('click', async (e) => {
     const dropdownBtn = e.target.closest('.ui.dropdown.button');
     dropdownBtn.classList.add('is-loading');
 
diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js
index 47e1539ddc..da7cd6b2c0 100644
--- a/web_src/js/features/code-frequency.js
+++ b/web_src/js/features/code-frequency.js
@@ -1,7 +1,7 @@
 import {createApp} from 'vue';
 
 export async function initRepoCodeFrequency() {
-  const el = document.getElementById('repo-code-frequency-chart');
+  const el = document.querySelector('#repo-code-frequency-chart');
   if (!el) return;
 
   const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue');
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
index 6d00d908c9..a85c04de41 100644
--- a/web_src/js/features/colorpicker.js
+++ b/web_src/js/features/colorpicker.js
@@ -1,7 +1,7 @@
 import {createTippy} from '../modules/tippy.js';
 
 export async function initColorPickers() {
-  const els = document.getElementsByClassName('js-color-picker-input');
+  const els = document.querySelectorAll('.js-color-picker-input');
   if (!els.length) return;
 
   await Promise.all([
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 5162c71509..1ab2a55699 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -24,8 +24,8 @@ export function initGlobalFormDirtyLeaveConfirm() {
 }
 
 export function initHeadNavbarContentToggle() {
-  const navbar = document.getElementById('navbar');
-  const btn = document.getElementById('navbar-expand-toggle');
+  const navbar = document.querySelector('#navbar');
+  const btn = document.querySelector('#navbar-expand-toggle');
   if (!navbar || !btn) return;
 
   btn.addEventListener('click', () => {
diff --git a/web_src/js/features/common-issue-list.js b/web_src/js/features/common-issue-list.js
index 219a8a9c9a..707776487b 100644
--- a/web_src/js/features/common-issue-list.js
+++ b/web_src/js/features/common-issue-list.js
@@ -29,7 +29,7 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) {
 }
 
 export function initCommonIssueListQuickGoto() {
-  const goto = document.getElementById('issue-list-quick-goto');
+  const goto = document.querySelector('#issue-list-quick-goto');
   if (!goto) return;
 
   const form = goto.closest('form');
diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js
index 081c47425f..7ef23fe4b0 100644
--- a/web_src/js/features/comp/SearchUserBox.js
+++ b/web_src/js/features/comp/SearchUserBox.js
@@ -5,7 +5,7 @@ const {appSubUrl} = window.config;
 const looksLikeEmailAddressCheck = /^\S+@\S+$/;
 
 export function initCompSearchUserBox() {
-  const searchUserBox = document.getElementById('search-user-box');
+  const searchUserBox = document.querySelector('#search-user-box');
   if (!searchUserBox) return;
 
   const $searchUserBox = $(searchUserBox);
diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js
index d74b59fd2a..38ff75e5a3 100644
--- a/web_src/js/features/comp/WebHookEditor.js
+++ b/web_src/js/features/comp/WebHookEditor.js
@@ -23,18 +23,18 @@ export function initCompWebHookEditor() {
   }
 
   // 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');
+  const httpMethodInput = document.querySelector('#http_method');
   if (httpMethodInput) {
     const updateContentType = function () {
       const visible = httpMethodInput.value === 'POST';
-      toggleElem(document.getElementById('content_type').closest('.field'), visible);
+      toggleElem(document.querySelector('#content_type').closest('.field'), visible);
     };
     updateContentType();
     httpMethodInput.addEventListener('change', updateContentType);
   }
 
   // Test delivery
-  document.getElementById('test-delivery')?.addEventListener('click', async function () {
+  document.querySelector('#test-delivery')?.addEventListener('click', async function () {
     this.classList.add('is-loading', 'disabled');
     await POST(this.getAttribute('data-link'));
     setTimeout(() => {
diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js
index 79b3389fee..475c66e900 100644
--- a/web_src/js/features/contributors.js
+++ b/web_src/js/features/contributors.js
@@ -1,7 +1,7 @@
 import {createApp} from 'vue';
 
 export async function initRepoContributors() {
-  const el = document.getElementById('repo-contributors-chart');
+  const el = document.querySelector('#repo-contributors-chart');
   if (!el) return;
 
   const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js
index 03efe00701..ea1e5cf7d0 100644
--- a/web_src/js/features/copycontent.js
+++ b/web_src/js/features/copycontent.js
@@ -6,7 +6,7 @@ import {GET} from '../modules/fetch.js';
 const {i18n} = window.config;
 
 export function initCopyContent() {
-  const btn = document.getElementById('copy-content');
+  const btn = document.querySelector('#copy-content');
   if (!btn || btn.classList.contains('disabled')) return;
 
   btn.addEventListener('click', async () => {
diff --git a/web_src/js/features/heatmap.js b/web_src/js/features/heatmap.js
index 719eeb75fb..9155e844a2 100644
--- a/web_src/js/features/heatmap.js
+++ b/web_src/js/features/heatmap.js
@@ -3,7 +3,7 @@ import ActivityHeatmap from '../components/ActivityHeatmap.vue';
 import {translateMonth, translateDay} from '../utils.js';
 
 export function initHeatmap() {
-  const el = document.getElementById('user-heatmap');
+  const el = document.querySelector('#user-heatmap');
   if (!el) return;
 
   try {
diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js
index 54ba3778f8..6354db6cdc 100644
--- a/web_src/js/features/install.js
+++ b/web_src/js/features/install.js
@@ -22,12 +22,12 @@ function initPreInstall() {
     mssql: '127.0.0.1:1433',
   };
 
-  const dbHost = document.getElementById('db_host');
-  const dbUser = document.getElementById('db_user');
-  const dbName = document.getElementById('db_name');
+  const dbHost = document.querySelector('#db_host');
+  const dbUser = document.querySelector('#db_user');
+  const dbName = document.querySelector('#db_name');
 
   // Database type change detection.
-  document.getElementById('db_type').addEventListener('change', function () {
+  document.querySelector('#db_type').addEventListener('change', function () {
     const dbType = this.value;
     hideElem('div[data-db-setting-for]');
     showElem(`div[data-db-setting-for=${dbType}]`);
@@ -46,14 +46,14 @@ function initPreInstall() {
       }
     } // else: for SQLite3, the default path is always prepared by backend code (setting)
   });
-  document.getElementById('db_type').dispatchEvent(new Event('change'));
+  document.querySelector('#db_type').dispatchEvent(new Event('change'));
 
-  const appUrl = document.getElementById('app_url');
+  const appUrl = document.querySelector('#app_url');
   if (appUrl.value.includes('://localhost')) {
     appUrl.value = window.location.href;
   }
 
-  const domain = document.getElementById('domain');
+  const domain = document.querySelector('#domain');
   if (domain.value.trim() === 'localhost') {
     domain.value = window.location.hostname;
   }
@@ -103,7 +103,7 @@ function initPreInstall() {
 }
 
 function initPostInstall() {
-  const el = document.getElementById('goto-user-login');
+  const el = document.querySelector('#goto-user-login');
   if (!el) return;
 
   const targetUrl = el.getAttribute('href');
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index f045879dec..c22fc17306 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -7,13 +7,13 @@ const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
 let notificationSequenceNumber = 0;
 
 export function initNotificationsTable() {
-  const table = document.getElementById('notification_table');
+  const table = document.querySelector('#notification_table');
   if (!table) return;
 
   // when page restores from bfcache, delete previously clicked items
   window.addEventListener('pageshow', (e) => {
     if (e.persisted) { // page was restored from bfcache
-      const table = document.getElementById('notification_table');
+      const table = document.querySelector('#notification_table');
       const unreadCountEl = document.querySelector('.notifications-unread-count');
       let unreadCount = parseInt(unreadCountEl.textContent);
       for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) {
@@ -145,7 +145,7 @@ async function updateNotificationCountWithCallback(callback, timeout, lastCount)
 }
 
 async function updateNotificationTable() {
-  const notificationDiv = document.getElementById('notification_div');
+  const notificationDiv = document.querySelector('#notification_div');
   if (notificationDiv) {
     try {
       const params = new URLSearchParams(window.location.search);
@@ -181,7 +181,7 @@ async function updateNotificationCount() {
 
     toggleElem('.notification_count', data.new !== 0);
 
-    for (const el of document.getElementsByClassName('notification_count')) {
+    for (const el of document.querySelectorAll('.notification_count')) {
       el.textContent = `${data.new}`;
     }
 
diff --git a/web_src/js/features/pull-view-file.js b/web_src/js/features/pull-view-file.js
index 2472e5a0bd..84c5eddb45 100644
--- a/web_src/js/features/pull-view-file.js
+++ b/web_src/js/features/pull-view-file.js
@@ -12,9 +12,9 @@ const collapseFilesBtnSelector = '#collapse-files-btn';
 // Refreshes the summary of viewed files if present
 // The data used will be window.config.pageData.prReview.numberOf{Viewed}Files
 function refreshViewedFilesSummary() {
-  const viewedFilesProgress = document.getElementById('viewed-files-summary');
+  const viewedFilesProgress = document.querySelector('#viewed-files-summary');
   viewedFilesProgress?.setAttribute('value', prReview.numberOfViewedFiles);
-  const summaryLabel = document.getElementById('viewed-files-summary-label');
+  const summaryLabel = document.querySelector('#viewed-files-summary-label');
   if (summaryLabel) summaryLabel.innerHTML = summaryLabel.getAttribute('data-text-changed-template')
     .replace('%[1]d', prReview.numberOfViewedFiles)
     .replace('%[2]d', prReview.numberOfFiles);
diff --git a/web_src/js/features/recent-commits.js b/web_src/js/features/recent-commits.js
index 030c251a05..b7f7c49987 100644
--- a/web_src/js/features/recent-commits.js
+++ b/web_src/js/features/recent-commits.js
@@ -1,7 +1,7 @@
 import {createApp} from 'vue';
 
 export async function initRepoRecentCommits() {
-  const el = document.getElementById('repo-recent-commits-chart');
+  const el = document.querySelector('#repo-recent-commits-chart');
   if (!el) return;
 
   const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue');
diff --git a/web_src/js/features/repo-diff-commitselect.js b/web_src/js/features/repo-diff-commitselect.js
index ebac64e855..2d0d63946c 100644
--- a/web_src/js/features/repo-diff-commitselect.js
+++ b/web_src/js/features/repo-diff-commitselect.js
@@ -2,7 +2,7 @@ import {createApp} from 'vue';
 import DiffCommitSelector from '../components/DiffCommitSelector.vue';
 
 export function initDiffCommitSelect() {
-  const el = document.getElementById('diff-commit-select');
+  const el = document.querySelector('#diff-commit-select');
   if (!el) return;
 
   const commitSelect = createApp(DiffCommitSelector);
diff --git a/web_src/js/features/repo-diff-filetree.js b/web_src/js/features/repo-diff-filetree.js
index 5dd2c42e74..6d9533d066 100644
--- a/web_src/js/features/repo-diff-filetree.js
+++ b/web_src/js/features/repo-diff-filetree.js
@@ -3,13 +3,13 @@ import DiffFileTree from '../components/DiffFileTree.vue';
 import DiffFileList from '../components/DiffFileList.vue';
 
 export function initDiffFileTree() {
-  const el = document.getElementById('diff-file-tree');
+  const el = document.querySelector('#diff-file-tree');
   if (!el) return;
 
   const fileTreeView = createApp(DiffFileTree);
   fileTreeView.mount(el);
 
-  const fileListElement = document.getElementById('diff-file-list');
+  const fileListElement = document.querySelector('#diff-file-list');
   if (!fileListElement) return;
 
   const fileListView = createApp(DiffFileList);
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 00f74515df..cd01232a7e 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -13,7 +13,7 @@ import {POST, GET} from '../modules/fetch.js';
 const {pageData, i18n} = window.config;
 
 function initRepoDiffReviewButton() {
-  const reviewBox = document.getElementById('review-box');
+  const reviewBox = document.querySelector('#review-box');
   if (!reviewBox) return;
 
   const counter = reviewBox.querySelector('.review-comments-counter');
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index b4fae4f6aa..aa9ca657b0 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -112,7 +112,7 @@ export function initRepoEditor() {
 
     // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
     // to enable or disable the commit button
-    const commitButton = document.getElementById('commit-button');
+    const commitButton = document.querySelector('#commit-button');
     const $editForm = $('.ui.edit.form');
     const dirtyFileClass = 'dirty-file';
 
diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js
index cff5068a1e..945eeeceff 100644
--- a/web_src/js/features/repo-findfile.js
+++ b/web_src/js/features/repo-findfile.js
@@ -106,11 +106,11 @@ async function loadRepoFiles() {
 }
 
 export function initFindFileInRepo() {
-  repoFindFileInput = document.getElementById('repo-file-find-input');
+  repoFindFileInput = document.querySelector('#repo-file-find-input');
   if (!repoFindFileInput) return;
 
   repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody');
-  repoFindFileNoResult = document.getElementById('repo-find-file-no-result');
+  repoFindFileNoResult = document.querySelector('#repo-find-file-no-result');
   repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value));
 
   loadRepoFiles();
diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js
index 0086b92021..7084e40977 100644
--- a/web_src/js/features/repo-graph.js
+++ b/web_src/js/features/repo-graph.js
@@ -3,12 +3,12 @@ import {hideElem, showElem} from '../utils/dom.js';
 import {GET} from '../modules/fetch.js';
 
 export function initRepoGraphGit() {
-  const graphContainer = document.getElementById('git-graph-container');
+  const graphContainer = document.querySelector('#git-graph-container');
   if (!graphContainer) return;
 
-  document.getElementById('flow-color-monochrome')?.addEventListener('click', () => {
-    document.getElementById('flow-color-monochrome').classList.add('active');
-    document.getElementById('flow-color-colored')?.classList.remove('active');
+  document.querySelector('#flow-color-monochrome')?.addEventListener('click', () => {
+    document.querySelector('#flow-color-monochrome').classList.add('active');
+    document.querySelector('#flow-color-colored')?.classList.remove('active');
     graphContainer.classList.remove('colored');
     graphContainer.classList.add('monochrome');
     const params = new URLSearchParams(window.location.search);
@@ -30,9 +30,9 @@ export function initRepoGraphGit() {
     }
   });
 
-  document.getElementById('flow-color-colored')?.addEventListener('click', () => {
-    document.getElementById('flow-color-colored').classList.add('active');
-    document.getElementById('flow-color-monochrome')?.classList.remove('active');
+  document.querySelector('#flow-color-colored')?.addEventListener('click', () => {
+    document.querySelector('#flow-color-colored').classList.add('active');
+    document.querySelector('#flow-color-monochrome')?.classList.remove('active');
     graphContainer.classList.add('colored');
     graphContainer.classList.remove('monochrome');
     for (const link of document.querySelectorAll('.pagination a')) {
@@ -60,7 +60,7 @@ export function initRepoGraphGit() {
     const ajaxUrl = new URL(url);
     ajaxUrl.searchParams.set('div-only', 'true');
     window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname);
-    document.getElementById('pagination').innerHTML = '';
+    document.querySelector('#pagination').innerHTML = '';
     hideElem('#rel-container');
     hideElem('#rev-container');
     showElem('#loading-indicator');
@@ -69,9 +69,9 @@ export function initRepoGraphGit() {
       const html = await response.text();
       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;
+      document.querySelector('#pagination').innerHTML = div.querySelector('#pagination').innerHTML;
+      document.querySelector('#rel-container').innerHTML = div.querySelector('#rel-container').innerHTML;
+      document.querySelector('#rev-container').innerHTML = div.querySelector('#rev-container').innerHTML;
       hideElem('#loading-indicator');
       showElem('#rel-container');
       showElem('#rev-container');
@@ -82,7 +82,7 @@ export function initRepoGraphGit() {
     dropdownSelected.splice(0, 0, '...flow-hide-pr-refs');
   }
 
-  const flowSelectRefsDropdown = document.getElementById('flow-select-refs-dropdown');
+  const flowSelectRefsDropdown = document.querySelector('#flow-select-refs-dropdown');
   $(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected);
   $(flowSelectRefsDropdown).dropdown({
     clearable: true,
@@ -115,7 +115,7 @@ export function initRepoGraphGit() {
     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');
+      document.querySelector(`#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');
@@ -136,7 +136,7 @@ export function initRepoGraphGit() {
     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');
+      document.querySelector(`#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');
diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js
index 6a5bce8268..f48c1b1bb3 100644
--- a/web_src/js/features/repo-home.js
+++ b/web_src/js/features/repo-home.js
@@ -7,11 +7,11 @@ import {showErrorToast} from '../modules/toast.js';
 const {appSubUrl} = window.config;
 
 export function initRepoTopicBar() {
-  const mgrBtn = document.getElementById('manage_topic');
+  const mgrBtn = document.querySelector('#manage_topic');
   if (!mgrBtn) return;
 
-  const editDiv = document.getElementById('topic_edit');
-  const viewDiv = document.getElementById('repo-topics');
+  const editDiv = document.querySelector('#topic_edit');
+  const viewDiv = document.querySelector('#repo-topics');
   const topicDropdown = editDiv.querySelector('.ui.dropdown');
   let lastErrorToast;
 
@@ -28,7 +28,7 @@ export function initRepoTopicBar() {
     mgrBtn.focus();
   });
 
-  document.getElementById('save_topic').addEventListener('click', async (e) => {
+  document.querySelector('#save_topic').addEventListener('click', async (e) => {
     lastErrorToast?.hideToast();
     const topics = editDiv.querySelector('input[name=topics]').value;
 
diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js
index 29b96f5127..8d43b6620c 100644
--- a/web_src/js/features/repo-issue-edit.js
+++ b/web_src/js/features/repo-issue-edit.js
@@ -55,7 +55,7 @@ async function onEditContent(event) {
           dropzone.querySelector('.files').append(input);
         });
         this.on('removedfile', async (file) => {
-          document.getElementById(file.uuid)?.remove();
+          document.querySelector(`#${file.uuid}`)?.remove();
           if (disableRemovedfileEvent) return;
           if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
             try {
@@ -137,7 +137,7 @@ async function onEditContent(event) {
       }
       editContentZone.setAttribute('data-content-version', data.contentVersion);
       if (!data.content) {
-        renderContent.innerHTML = document.getElementById('no-content').innerHTML;
+        renderContent.innerHTML = document.querySelector('#no-content').innerHTML;
         rawContent.textContent = '';
       } else {
         renderContent.innerHTML = data.content;
@@ -166,7 +166,7 @@ async function onEditContent(event) {
 
   comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
   if (!comboMarkdownEditor) {
-    editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
+    editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML;
     comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
     comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
     editContentZone.querySelector('.ui.cancel.button').addEventListener('click', cancelAndReset);
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 5d18a7ff8d..c8ae91d453 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -158,7 +158,7 @@ function initRepoIssueListAuthorDropdown() {
 }
 
 function initPinRemoveButton() {
-  for (const button of document.getElementsByClassName('issue-card-unpin')) {
+  for (const button of document.querySelectorAll('.issue-card-unpin')) {
     button.addEventListener('click', async (event) => {
       const el = event.currentTarget;
       const id = Number(el.getAttribute('data-issue-id'));
@@ -182,7 +182,7 @@ async function pinMoveEnd(e) {
 }
 
 async function initIssuePinSort() {
-  const pinDiv = document.getElementById('issue-pins');
+  const pinDiv = document.querySelector('#issue-pins');
 
   if (pinDiv === null) return;
 
diff --git a/web_src/js/features/repo-issue-pr-form.js b/web_src/js/features/repo-issue-pr-form.js
index 7b26e643c0..94a2857340 100644
--- a/web_src/js/features/repo-issue-pr-form.js
+++ b/web_src/js/features/repo-issue-pr-form.js
@@ -2,7 +2,7 @@ import {createApp} from 'vue';
 import PullRequestMergeForm from '../components/PullRequestMergeForm.vue';
 
 export function initRepoPullRequestMergeForm() {
-  const el = document.getElementById('pull-request-merge-form');
+  const el = document.querySelector('#pull-request-merge-form');
   if (!el) return;
 
   const view = createApp(PullRequestMergeForm);
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 3cbbdc41fc..d53c3346f3 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -44,14 +44,14 @@ export function initRepoIssueTimeTracking() {
 
 async function updateDeadline(deadlineString) {
   hideElem('#deadline-err-invalid-date');
-  document.getElementById('deadline-loader')?.classList.add('is-loading');
+  document.querySelector('#deadline-loader')?.classList.add('is-loading');
 
   let realDeadline = null;
   if (deadlineString !== '') {
     const newDate = Date.parse(deadlineString);
 
     if (Number.isNaN(newDate)) {
-      document.getElementById('deadline-loader')?.classList.remove('is-loading');
+      document.querySelector('#deadline-loader')?.classList.remove('is-loading');
       showElem('#deadline-err-invalid-date');
       return false;
     }
@@ -59,7 +59,7 @@ async function updateDeadline(deadlineString) {
   }
 
   try {
-    const response = await POST(document.getElementById('update-issue-deadline-form').getAttribute('action'), {
+    const response = await POST(document.querySelector('#update-issue-deadline-form').getAttribute('action'), {
       data: {due_date: realDeadline},
     });
 
@@ -70,7 +70,7 @@ async function updateDeadline(deadlineString) {
     }
   } catch (error) {
     console.error(error);
-    document.getElementById('deadline-loader').classList.remove('is-loading');
+    document.querySelector('#deadline-loader').classList.remove('is-loading');
     showElem('#deadline-err-invalid-date');
   }
 }
@@ -182,7 +182,7 @@ export function initRepoIssueCommentDelete() {
           counter.textContent = String(num);
         }
 
-        document.getElementById(deleteButton.getAttribute('data-comment-id'))?.remove();
+        document.querySelector(`#${deleteButton.getAttribute('data-comment-id')}`)?.remove();
 
         if (conversationHolder && !conversationHolder.querySelector('.comment')) {
           const path = conversationHolder.getAttribute('data-path');
@@ -298,7 +298,7 @@ export function initRepoPullRequestMergeInstruction() {
 }
 
 export function initRepoPullRequestAllowMaintainerEdit() {
-  const wrapper = document.getElementById('allow-edits-from-maintainers');
+  const wrapper = document.querySelector('#allow-edits-from-maintainers');
   if (!wrapper) return;
   const checkbox = wrapper.querySelector('input[type="checkbox"]');
   checkbox.addEventListener('input', async () => {
@@ -678,7 +678,7 @@ 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 = document.getElementById('status-button');
+  const statusButton = document.querySelector('#status-button');
   if (statusButton) {
     opts.onContentChanged = (editor) => {
       const statusText = statusButton.getAttribute(editor.value().trim() ? 'data-status-and-comment' : 'data-status');
diff --git a/web_src/js/features/repo-migrate.js b/web_src/js/features/repo-migrate.js
index 490e7df0e4..b8157e2dad 100644
--- a/web_src/js/features/repo-migrate.js
+++ b/web_src/js/features/repo-migrate.js
@@ -4,10 +4,10 @@ import {GET, POST} from '../modules/fetch.js';
 const {appSubUrl} = window.config;
 
 export function initRepoMigrationStatusChecker() {
-  const repoMigrating = document.getElementById('repo_migrating');
+  const repoMigrating = document.querySelector('#repo_migrating');
   if (!repoMigrating) return;
 
-  document.getElementById('repo_migrating_retry').addEventListener('click', doMigrationRetry);
+  document.querySelector('#repo_migrating_retry').addEventListener('click', doMigrationRetry);
 
   const task = repoMigrating.getAttribute('data-migrating-task-id');
 
@@ -20,7 +20,7 @@ export function initRepoMigrationStatusChecker() {
 
     // for all status
     if (data.message) {
-      document.getElementById('repo_migrating_progress_message').textContent = data.message;
+      document.querySelector('#repo_migrating_progress_message').textContent = data.message;
     }
 
     // TaskStatusFinished
@@ -36,7 +36,7 @@ export function initRepoMigrationStatusChecker() {
       showElem('#repo_migrating_retry');
       showElem('#repo_migrating_failed');
       showElem('#repo_migrating_failed_image');
-      document.getElementById('repo_migrating_failed_error').textContent = data.message;
+      document.querySelector('#repo_migrating_failed_error').textContent = data.message;
       return false;
     }
 
diff --git a/web_src/js/features/repo-migration.js b/web_src/js/features/repo-migration.js
index 59e282e4e7..7f7aa237ee 100644
--- a/web_src/js/features/repo-migration.js
+++ b/web_src/js/features/repo-migration.js
@@ -1,13 +1,13 @@
 import {hideElem, showElem, toggleElem} from '../utils/dom.js';
 
-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 service = document.querySelector('#service_type');
+const user = document.querySelector('#auth_username');
+const pass = document.querySelector('#auth_password');
+const token = document.querySelector('#auth_token');
+const mirror = document.querySelector('#mirror');
+const lfs = document.querySelector('#lfs');
+const lfsSettings = document.querySelector('#lfs_settings');
+const lfsEndpoint = document.querySelector('#lfs_endpoint');
 const items = document.querySelectorAll('#migrate_items input[type=checkbox]');
 
 export function initRepoMigration() {
@@ -18,16 +18,16 @@ export function initRepoMigration() {
   pass?.addEventListener('input', () => {checkItems(false)});
   token?.addEventListener('input', () => {checkItems(true)});
   mirror?.addEventListener('change', () => {checkItems(true)});
-  document.getElementById('lfs_settings_show')?.addEventListener('click', (e) => {
+  document.querySelector('#lfs_settings_show')?.addEventListener('click', (e) => {
     e.preventDefault();
     e.stopPropagation();
     showElem(lfsEndpoint);
   });
   lfs?.addEventListener('change', setLFSSettingsVisibility);
 
-  const cloneAddr = document.getElementById('clone_addr');
+  const cloneAddr = document.querySelector('#clone_addr');
   cloneAddr?.addEventListener('change', () => {
-    const repoName = document.getElementById('repo_name');
+    const repoName = document.querySelector('#repo_name');
     if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank
       repoName.value = cloneAddr.value.match(/^(.*\/)?((.+?)(\.git)?)$/)[3];
     }
diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js
index a1cc4b346b..706942363d 100644
--- a/web_src/js/features/repo-projects.js
+++ b/web_src/js/features/repo-projects.js
@@ -5,8 +5,8 @@ import {POST, DELETE, PUT} from '../modules/fetch.js';
 
 function updateIssueCount(cards) {
   const parent = cards.parentElement;
-  const cnt = parent.getElementsByClassName('issue-card').length;
-  parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt;
+  const cnt = parent.querySelectorAll('.issue-card').length;
+  parent.querySelectorAll('.project-column-issue-count')[0].textContent = cnt;
 }
 
 async function createNewColumn(url, columnTitle, projectColorInput) {
@@ -26,7 +26,7 @@ async function createNewColumn(url, columnTitle, projectColorInput) {
 }
 
 async function moveIssue({item, from, to, oldIndex}) {
-  const columnCards = to.getElementsByClassName('issue-card');
+  const columnCards = to.querySelectorAll('.issue-card');
   updateIssueCount(from);
   updateIssueCount(to);
 
@@ -53,7 +53,7 @@ async function initRepoProjectSortable() {
 
   // the HTML layout is: #project-board > .board > .project-column .cards > .issue-card
   const mainBoard = els[0];
-  let boardColumns = mainBoard.getElementsByClassName('project-column');
+  let boardColumns = mainBoard.querySelectorAll('.project-column');
   createSortable(mainBoard, {
     group: 'project-column',
     draggable: '.project-column',
@@ -61,7 +61,7 @@ async function initRepoProjectSortable() {
     delayOnTouchOnly: true,
     delay: 500,
     onSort: async () => {
-      boardColumns = mainBoard.getElementsByClassName('project-column');
+      boardColumns = mainBoard.querySelectorAll('.project-column');
 
       const columnSorting = {
         columns: Array.from(boardColumns, (column, i) => ({
@@ -81,7 +81,7 @@ async function initRepoProjectSortable() {
   });
 
   for (const boardColumn of boardColumns) {
-    const boardCardList = boardColumn.getElementsByClassName('cards')[0];
+    const boardCardList = boardColumn.querySelectorAll('.cards')[0];
     createSortable(boardCardList, {
       group: 'shared',
       onAdd: moveIssue,
@@ -99,7 +99,7 @@ export function initRepoProject() {
 
   const _promise = initRepoProjectSortable();
 
-  for (const modal of document.getElementsByClassName('edit-project-column-modal')) {
+  for (const modal of document.querySelectorAll('.edit-project-column-modal')) {
     const projectHeader = modal.closest('.project-column-header');
     const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
     const projectTitleInput = modal.querySelector('.project-column-title-input');
diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js
index f3cfa74418..2be1ec58c6 100644
--- a/web_src/js/features/repo-release.js
+++ b/web_src/js/features/repo-release.js
@@ -20,7 +20,7 @@ export function initRepoReleaseNew() {
 }
 
 function initTagNameEditor() {
-  const el = document.getElementById('tag-name-editor');
+  const el = document.querySelector('#tag-name-editor');
   if (!el) return;
 
   const existingTags = JSON.parse(el.getAttribute('data-existing-tags'));
@@ -30,10 +30,10 @@ function initTagNameEditor() {
   const newTagHelperText = el.getAttribute('data-tag-helper-new');
   const existingTagHelperText = el.getAttribute('data-tag-helper-existing');
 
-  const tagNameInput = document.getElementById('tag-name');
+  const tagNameInput = document.querySelector('#tag-name');
   const hideTargetInput = function(tagNameInput) {
     const value = tagNameInput.value;
-    const tagHelper = document.getElementById('tag-helper');
+    const tagHelper = document.querySelector('#tag-helper');
     if (existingTags.includes(value)) {
       // If the tag already exists, hide the target branch selector.
       hideElem('#tag-target-selector');
diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js
index 652f8ac290..6590c2b56c 100644
--- a/web_src/js/features/repo-settings.js
+++ b/web_src/js/features/repo-settings.js
@@ -44,7 +44,7 @@ export function initRepoSettingsCollaboration() {
 }
 
 export function initRepoSettingSearchTeamBox() {
-  const searchTeamBox = document.getElementById('search-team-box');
+  const searchTeamBox = document.querySelector('#search-team-box');
   if (!searchTeamBox) return;
 
   $(searchTeamBox).search({
@@ -78,29 +78,29 @@ export function initRepoSettingGitHook() {
 export function initRepoSettingBranches() {
   if (!document.querySelector('.repository.settings.branches')) return;
 
-  for (const el of document.getElementsByClassName('toggle-target-enabled')) {
+  for (const el of document.querySelectorAll('.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')) {
+  for (const el of document.querySelectorAll('.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);
+  document.querySelector('#dismiss_stale_approvals')?.addEventListener('change', function () {
+    document.querySelector('#ignore_stale_approvals_box')?.classList.toggle('disabled', this.checked);
   });
 
   // show the `Matched` mark for the status checks that match the pattern
   const markMatchedStatusChecks = () => {
-    const patterns = (document.getElementById('status_check_contexts').value || '').split(/[\r\n]+/);
+    const patterns = (document.querySelector('#status_check_contexts').value || '').split(/[\r\n]+/);
     const validPatterns = patterns.map((item) => item.trim()).filter(Boolean);
-    const marks = document.getElementsByClassName('status-check-matched-mark');
+    const marks = document.querySelectorAll('.status-check-matched-mark');
 
     for (const el of marks) {
       let matched = false;
@@ -115,5 +115,5 @@ export function initRepoSettingBranches() {
     }
   };
   markMatchedStatusChecks();
-  document.getElementById('status_check_contexts').addEventListener('input', onInputDebounce(markMatchedStatusChecks));
+  document.querySelector('#status_check_contexts').addEventListener('input', onInputDebounce(markMatchedStatusChecks));
 }
diff --git a/web_src/js/features/sshkey-helper.js b/web_src/js/features/sshkey-helper.js
index 3960eefe8e..5531c18451 100644
--- a/web_src/js/features/sshkey-helper.js
+++ b/web_src/js/features/sshkey-helper.js
@@ -1,8 +1,8 @@
 export function initSshKeyFormParser() {
   // Parse SSH Key
-  document.getElementById('ssh-key-content')?.addEventListener('input', function () {
+  document.querySelector('#ssh-key-content')?.addEventListener('input', function () {
     const arrays = this.value.split(' ');
-    const title = document.getElementById('ssh-key-title');
+    const title = document.querySelector('#ssh-key-title');
     if (!title.value && arrays.length === 3 && arrays[2] !== '') {
       title.value = arrays[2];
     }
diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js
index 6dfbb4d765..ea26614ba7 100644
--- a/web_src/js/features/user-auth-webauthn.js
+++ b/web_src/js/features/user-auth-webauthn.js
@@ -109,7 +109,7 @@ async function webauthnRegistered(newCredential) {
 }
 
 function webAuthnError(errorType, message) {
-  const elErrorMsg = document.getElementById(`webauthn-error-msg`);
+  const elErrorMsg = document.querySelector(`#webauthn-error-msg`);
 
   if (errorType === 'general') {
     elErrorMsg.textContent = message || 'unknown error';
@@ -140,7 +140,7 @@ function detectWebAuthnSupport() {
 }
 
 export function initUserAuthWebAuthnRegister() {
-  const elRegister = document.getElementById('register-webauthn');
+  const elRegister = document.querySelector('#register-webauthn');
   if (!elRegister) {
     return;
   }
@@ -155,7 +155,7 @@ export function initUserAuthWebAuthnRegister() {
 }
 
 async function webAuthnRegisterRequest() {
-  const elNickname = document.getElementById('nickname');
+  const elNickname = document.querySelector('#nickname');
 
   const formData = new FormData();
   formData.append('name', elNickname.value);
diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js
index a871ac471c..1ea131e75f 100644
--- a/web_src/js/features/user-auth.js
+++ b/web_src/js/features/user-auth.js
@@ -1,9 +1,9 @@
 import {checkAppUrl} from './common-global.js';
 
 export function initUserAuthOauth2() {
-  const outer = document.getElementById('oauth2-login-navigator');
+  const outer = document.querySelector('#oauth2-login-navigator');
   if (!outer) return;
-  const inner = document.getElementById('oauth2-login-navigator-inner');
+  const inner = document.querySelector('#oauth2-login-navigator-inner');
 
   checkAppUrl();
 
diff --git a/web_src/js/features/user-settings.js b/web_src/js/features/user-settings.js
index 2d8c53e457..8cb1f0582f 100644
--- a/web_src/js/features/user-settings.js
+++ b/web_src/js/features/user-settings.js
@@ -3,11 +3,11 @@ import {hideElem, showElem} from '../utils/dom.js';
 export function initUserSettings() {
   if (!document.querySelectorAll('.user.settings.profile').length) return;
 
-  const usernameInput = document.getElementById('username');
+  const usernameInput = document.querySelector('#username');
   if (!usernameInput) return;
   usernameInput.addEventListener('input', function () {
-    const prompt = document.getElementById('name-change-prompt');
-    const promptRedirect = document.getElementById('name-change-redirect-prompt');
+    const prompt = document.querySelector('#name-change-prompt');
+    const promptRedirect = document.querySelector('#name-change-redirect-prompt');
     if (this.value.toLowerCase() !== this.getAttribute('data-name').toLowerCase()) {
       showElem(prompt);
       showElem(promptRedirect);
diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
index 0e2c92713a..6f36d09683 100644
--- a/web_src/js/markup/anchors.js
+++ b/web_src/js/markup/anchors.js
@@ -9,19 +9,16 @@ function scrollToAnchor(encodedId) {
   if (!encodedId) return;
   const id = decodeURIComponent(encodedId);
   const prefixedId = addPrefix(id);
-  let el = document.getElementById(prefixedId);
+  let el = document.querySelector(`#${prefixedId}`);
 
   // check for matching user-generated `a[name]`
   if (!el) {
-    const nameAnchors = document.getElementsByName(prefixedId);
-    if (nameAnchors.length) {
-      el = nameAnchors[0];
-    }
+    el = document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`);
   }
 
   // compat for links with old 'user-content-' prefixed hashes
   if (!el && hasPrefix(id)) {
-    return document.getElementById(id)?.scrollIntoView();
+    return document.querySelector(`#${id}`)?.scrollIntoView();
   }
 
   el?.scrollIntoView();
diff --git a/web_src/js/standalone/devtest.js b/web_src/js/standalone/devtest.js
index d0ca511c0f..8dbba554ac 100644
--- a/web_src/js/standalone/devtest.js
+++ b/web_src/js/standalone/devtest.js
@@ -1,11 +1,11 @@
 import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js';
 
-document.getElementById('info-toast').addEventListener('click', () => {
+document.querySelector('#info-toast').addEventListener('click', () => {
   showInfoToast('success 😀');
 });
-document.getElementById('warning-toast').addEventListener('click', () => {
+document.querySelector('#warning-toast').addEventListener('click', () => {
   showWarningToast('warning 😐');
 });
-document.getElementById('error-toast').addEventListener('click', () => {
+document.querySelector('#error-toast').addEventListener('click', () => {
   showErrorToast('error 🙁');
 });
diff --git a/web_src/js/standalone/swagger.js b/web_src/js/standalone/swagger.js
index 00854ef5d7..2928813167 100644
--- a/web_src/js/standalone/swagger.js
+++ b/web_src/js/standalone/swagger.js
@@ -2,7 +2,7 @@ import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js';
 import 'swagger-ui-dist/swagger-ui.css';
 
 window.addEventListener('load', async () => {
-  const url = document.getElementById('swagger-ui').getAttribute('data-source');
+  const url = document.querySelector('#swagger-ui').getAttribute('data-source');
   const res = await fetch(url);
   const spec = await res.json();
 

From 1844dc6c1d4d40e2b7f493d56b5f4e371a835e38 Mon Sep 17 00:00:00 2001
From: GiteaBot <teabot@gitea.io>
Date: Tue, 11 Jun 2024 00:26:13 +0000
Subject: [PATCH 116/131] [skip ci] Updated translations via Crowdin

---
 options/locale/locale_fr-FR.ini | 12 ++++++------
 options/locale/locale_ja-JP.ini |  5 +++++
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 230107fc96..6dcc7a4f3f 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -363,14 +363,14 @@ filter_by_team_repositories=Dépôts filtrés par équipe
 feed_of=Flux de « %s »
 
 show_archived=Archivé
-show_both_archived_unarchived=Afficher à la fois archivé et non archivé
-show_only_archived=Afficher uniquement les archivés
-show_only_unarchived=Afficher uniquement les non archivés
+show_both_archived_unarchived=Afficher à la fois les dépôts archivés et non archivés
+show_only_archived=Afficher uniquement les dépôts archivés
+show_only_unarchived=Afficher uniquement les dépôts non archivés
 
 show_private=Privé
-show_both_private_public=Afficher les publics et privés
-show_only_private=Afficher uniquement les privés
-show_only_public=Afficher uniquement les publics
+show_both_private_public=Afficher les dépôts publics et privés
+show_only_private=Afficher uniquement les dépôts privés
+show_only_public=Afficher uniquement les dépôts publics
 
 issues.in_your_repos=Dans vos dépôts
 
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 89df5ac0b9..d85ffb4694 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -1378,6 +1378,7 @@ commitstatus.success=成功
 ext_issues=外部イシューへのアクセス
 ext_issues.desc=外部のイシュートラッカーへのリンク。
 
+projects.desc=プロジェクトでイシューとプルリクエストを管理します。
 projects.description=説明 (オプション)
 projects.description_placeholder=説明
 projects.create=プロジェクトを作成
@@ -1552,7 +1553,9 @@ 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
+issues.close_comment_issue=コメントしてクローズ
 issues.reopen_issue=再オープンする
+issues.reopen_comment_issue=コメントして再オープン
 issues.create_comment=コメントする
 issues.comment.blocked_user=投稿者またはリポジトリのオーナーがあなたをブロックしているため、コメントの作成や編集はできません。
 issues.closed_at=`がイシューをクローズ <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@@ -3412,6 +3415,7 @@ error.unit_not_allowed=このセクションへのアクセスが許可されて
 title=パッケージ
 desc=リポジトリ パッケージを管理します。
 empty=パッケージはまだありません。
+no_metadata=メタデータがありません。
 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> を参照してください。
@@ -3634,6 +3638,7 @@ runs.pushed_by=pushed by
 runs.invalid_workflow_helper=ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s
 runs.no_matching_online_runner_helper=ラベルに一致するオンラインのランナーが見つかりません: %s
 runs.no_job_without_needs=ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。
+runs.no_job=ワークフローには少なくとも1つのジョブが含まれている必要があります
 runs.actor=アクター
 runs.status=ステータス
 runs.actors_no_select=すべてのアクター

From 5342a61124bf2d4fbe4c1d560b13866198149ac9 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 11 Jun 2024 11:31:23 +0800
Subject: [PATCH 117/131] Delete legacy cookie before setting new cookie
 (#31306)

Try to fix #31202
---
 modules/web/middleware/cookie.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/web/middleware/cookie.go b/modules/web/middleware/cookie.go
index ec6b06f993..f2d25f5b1c 100644
--- a/modules/web/middleware/cookie.go
+++ b/modules/web/middleware/cookie.go
@@ -35,6 +35,10 @@ func GetSiteCookie(req *http.Request, name string) string {
 
 // SetSiteCookie returns given cookie value from request header.
 func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
+	// Previous versions would use a cookie path with a trailing /.
+	// These are more specific than cookies without a trailing /, so
+	// we need to delete these if they exist.
+	deleteLegacySiteCookie(resp, name)
 	cookie := &http.Cookie{
 		Name:     name,
 		Value:    url.QueryEscape(value),
@@ -46,10 +50,6 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
 		SameSite: setting.SessionConfig.SameSite,
 	}
 	resp.Header().Add("Set-Cookie", cookie.String())
-	// Previous versions would use a cookie path with a trailing /.
-	// These are more specific than cookies without a trailing /, so
-	// we need to delete these if they exist.
-	deleteLegacySiteCookie(resp, name)
 }
 
 // deleteLegacySiteCookie deletes the cookie with the given name at the cookie

From 397930d8c1ffaeefbfec438908b8ddfc75de249a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 11 Jun 2024 06:54:39 +0200
Subject: [PATCH 118/131] Fix line number width in code preview (#31307)

Line numbers were using some hacky CSS `width: 1%` that did nothing to
the code rendering as far as I can tell but broken the inline preview in
markup when line numbers are greater than 2 digits. Also I removed one
duplicate `font-family` rule (it is set below in the `.lines-num,
.lines-code` selector.
---
 web_src/css/base.css | 2 --
 1 file changed, 2 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 0e54d17262..3bdcde99f6 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1001,8 +1001,6 @@ overflow-menu .ui.label {
   padding: 0 8px;
   text-align: right !important;
   color: var(--color-text-light-2);
-  width: 1%;
-  font-family: var(--fonts-monospace);
 }
 
 .lines-num span.bottom-line::after {

From e6ab6e637fb4da2353522d192066869fc2a8a94b Mon Sep 17 00:00:00 2001
From: Kerwin Bryant <kerwin612@qq.com>
Date: Tue, 11 Jun 2024 21:07:10 +0800
Subject: [PATCH 119/131] code optimization (#31315)

Simplifying complex if-else to existing Iif operations
---
 modules/templates/helper.go                   | 31 +++++++++-
 modules/templates/helper_test.go              | 15 +++++
 templates/admin/auth/list.tmpl                |  2 +-
 templates/admin/config.tmpl                   | 56 +++++++++----------
 templates/admin/cron.tmpl                     |  2 +-
 templates/admin/emails/list.tmpl              |  6 +-
 templates/admin/user/list.tmpl                |  6 +-
 templates/repo/diff/whitespace_dropdown.tmpl  |  2 +-
 templates/repo/issue/filter_actions.tmpl      |  2 +-
 .../issue/labels/labels_selector_field.tmpl   |  4 +-
 .../repo/issue/view_content/sidebar.tmpl      |  2 +-
 templates/repo/issue/view_title.tmpl          |  2 +-
 templates/repo/settings/lfs_pointers.tmpl     |  6 +-
 templates/repo/star_unstar.tmpl               |  2 +-
 14 files changed, 90 insertions(+), 48 deletions(-)

diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 94464fe628..8779de69ca 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -9,6 +9,7 @@ import (
 	"html"
 	"html/template"
 	"net/url"
+	"reflect"
 	"slices"
 	"strings"
 	"time"
@@ -237,8 +238,8 @@ func DotEscape(raw string) string {
 
 // Iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version,
 // and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal).
-func Iif(condition bool, vals ...any) any {
-	if condition {
+func Iif(condition any, vals ...any) any {
+	if IsTruthy(condition) {
 		return vals[0]
 	} else if len(vals) > 1 {
 		return vals[1]
@@ -246,6 +247,32 @@ func Iif(condition bool, vals ...any) any {
 	return nil
 }
 
+func IsTruthy(v any) bool {
+	if v == nil {
+		return false
+	}
+
+	rv := reflect.ValueOf(v)
+	switch rv.Kind() {
+	case reflect.Bool:
+		return rv.Bool()
+	case reflect.String:
+		return rv.String() != ""
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return rv.Int() != 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return rv.Uint() != 0
+	case reflect.Float32, reflect.Float64:
+		return rv.Float() != 0
+	case reflect.Slice, reflect.Array, reflect.Map:
+		return rv.Len() > 0
+	case reflect.Ptr:
+		return !rv.IsNil() && IsTruthy(reflect.Indirect(rv).Interface())
+	default:
+		return rv.Kind() == reflect.Struct && !rv.IsNil()
+	}
+}
+
 // Eval the expression and return the result, see the comment of eval.Expr for details.
 // To use this helper function in templates, pass each token as a separate parameter.
 //
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index 0cefb7a6b2..c6c70cc18e 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -65,3 +65,18 @@ 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>`))
 }
+
+func TestIsTruthy(t *testing.T) {
+	var test any
+	assert.Equal(t, false, IsTruthy(test))
+	assert.Equal(t, false, IsTruthy(nil))
+	assert.Equal(t, false, IsTruthy(""))
+	assert.Equal(t, true, IsTruthy("non-empty"))
+	assert.Equal(t, true, IsTruthy(-1))
+	assert.Equal(t, false, IsTruthy(0))
+	assert.Equal(t, true, IsTruthy(42))
+	assert.Equal(t, false, IsTruthy(0.0))
+	assert.Equal(t, true, IsTruthy(3.14))
+	assert.Equal(t, false, IsTruthy([]int{}))
+	assert.Equal(t, true, IsTruthy([]int{1}))
+}
diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl
index 6483ec800c..174dda1e2a 100644
--- a/templates/admin/auth/list.tmpl
+++ b/templates/admin/auth/list.tmpl
@@ -25,7 +25,7 @@
 							<td>{{.ID}}</td>
 							<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{.Name}}</a></td>
 							<td>{{.TypeName}}</td>
-							<td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
+							<td>{{svg (Iif .IsActive "octicon-check" "octicon-x")}}</td>
 							<td>{{DateTime "short" .UpdatedUnix}}</td>
 							<td>{{DateTime "short" .CreatedUnix}}</td>
 							<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 8c16429920..197a6c6add 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -16,9 +16,9 @@
 				<dt>{{ctx.Locale.Tr "admin.config.domain"}}</dt>
 				<dd>{{.Domain}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.offline_mode"}}</dt>
-				<dd>{{if .OfflineMode}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .OfflineMode "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.disable_router_log"}}</dt>
-				<dd>{{if .DisableRouterLog}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .DisableRouterLog "octicon-check" "octicon-x")}}</dd>
 
 				<div class="divider"></div>
 
@@ -55,10 +55,10 @@
 		<div class="ui attached table segment">
 			<dl class="admin-dl-horizontal">
 				<dt>{{ctx.Locale.Tr "admin.config.ssh_enabled"}}</dt>
-				<dd>{{if not .SSH.Disabled}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif (not .SSH.Disabled) "octicon-check" "octicon-x")}}</dd>
 				{{if not .SSH.Disabled}}
 					<dt>{{ctx.Locale.Tr "admin.config.ssh_start_builtin_server"}}</dt>
-					<dd>{{if .SSH.StartBuiltinServer}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+					<dd>{{svg (Iif .SSH.StartBuiltinServer "octicon-check" "octicon-x")}}</dd>
 					<dt>{{ctx.Locale.Tr "admin.config.ssh_domain"}}</dt>
 					<dd>{{.SSH.Domain}}</dd>
 					<dt>{{ctx.Locale.Tr "admin.config.ssh_port"}}</dt>
@@ -74,7 +74,7 @@
 						<dt>{{ctx.Locale.Tr "admin.config.ssh_keygen_path"}}</dt>
 						<dd>{{.SSH.KeygenPath}}</dd>
 						<dt>{{ctx.Locale.Tr "admin.config.ssh_minimum_key_size_check"}}</dt>
-						<dd>{{if .SSH.MinimumKeySizeCheck}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+						<dd>{{svg (Iif .SSH.MinimumKeySizeCheck "octicon-check" "octicon-x")}}</dd>
 						{{if .SSH.MinimumKeySizeCheck}}
 							<dt>{{ctx.Locale.Tr "admin.config.ssh_minimum_key_sizes"}}</dt>
 							<dd>{{.SSH.MinimumKeySizes}}</dd>
@@ -90,7 +90,7 @@
 		<div class="ui attached table segment">
 			<dl class="admin-dl-horizontal">
 				<dt>{{ctx.Locale.Tr "admin.config.lfs_enabled"}}</dt>
-				<dd>{{if .LFS.StartServer}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .LFS.StartServer "octicon-check" "octicon-x")}}</dd>
 				{{if .LFS.StartServer}}
 					<dt>{{ctx.Locale.Tr "admin.config.lfs_content_path"}}</dt>
 					<dd>{{JsonUtils.EncodeToString .LFS.Storage.ToShadowCopy}}</dd>
@@ -134,36 +134,36 @@
 		<div class="ui attached table segment">
 			<dl class="admin-dl-horizontal">
 				<dt>{{ctx.Locale.Tr "admin.config.register_email_confirm"}}</dt>
-				<dd>{{if .Service.RegisterEmailConfirm}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.RegisterEmailConfirm "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.disable_register"}}</dt>
-				<dd>{{if .Service.DisableRegistration}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.DisableRegistration "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.allow_only_internal_registration"}}</dt>
-				<dd>{{if .Service.AllowOnlyInternalRegistration}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.AllowOnlyInternalRegistration "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.allow_only_external_registration"}}</dt>
-				<dd>{{if .Service.AllowOnlyExternalRegistration}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.AllowOnlyExternalRegistration "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.show_registration_button"}}</dt>
-				<dd>{{if .Service.ShowRegistrationButton}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.ShowRegistrationButton "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signup"}}</dt>
-				<dd>{{if .Service.EnableOpenIDSignUp}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.EnableOpenIDSignUp "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
-				<dd>{{if .Service.EnableOpenIDSignIn}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
-				<dd>{{if .Service.RequireSignInView}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
-				<dd>{{if .Service.EnableNotifyMail}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>
-				<dd>{{if .Service.EnableCaptcha}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.EnableCaptcha "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.default_keep_email_private"}}</dt>
-				<dd>{{if .Service.DefaultKeepEmailPrivate}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}</dt>
-				<dd>{{if .Service.DefaultAllowCreateOrganization}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.DefaultAllowCreateOrganization "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.enable_timetracking"}}</dt>
-				<dd>{{if .Service.EnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}</dd>
 				{{if .Service.EnableTimetracking}}
 					<dt>{{ctx.Locale.Tr "admin.config.default_enable_timetracking"}}</dt>
-					<dd>{{if .Service.DefaultEnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+					<dd>{{svg (Iif .Service.DefaultEnableTimetracking "octicon-check" "octicon-x")}}</dd>
 					<dt>{{ctx.Locale.Tr "admin.config.default_allow_only_contributors_to_track_time"}}</dt>
-					<dd>{{if .Service.DefaultAllowOnlyContributorsToTrackTime}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+					<dd>{{svg (Iif .Service.DefaultAllowOnlyContributorsToTrackTime "octicon-check" "octicon-x")}}</dd>
 				{{end}}
 				<dt>{{ctx.Locale.Tr "admin.config.default_visibility_organization"}}</dt>
 				<dd>{{.Service.DefaultOrgVisibility}}</dd>
@@ -171,7 +171,7 @@
 				<dt>{{ctx.Locale.Tr "admin.config.no_reply_address"}}</dt>
 				<dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.default_enable_dependencies"}}</dt>
-				<dd>{{if .Service.DefaultEnableDependencies}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Service.DefaultEnableDependencies "octicon-check" "octicon-x")}}</dd>
 				<div class="divider"></div>
 				<dt>{{ctx.Locale.Tr "admin.config.active_code_lives"}}</dt>
 				<dd>{{.Service.ActiveCodeLives}} {{ctx.Locale.Tr "tool.raw_minutes"}}</dd>
@@ -190,7 +190,7 @@
 				<dt>{{ctx.Locale.Tr "admin.config.deliver_timeout"}}</dt>
 				<dd>{{.Webhook.DeliverTimeout}} {{ctx.Locale.Tr "tool.raw_seconds"}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.skip_tls_verify"}}</dt>
-				<dd>{{if .Webhook.SkipTLSVerify}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Webhook.SkipTLSVerify "octicon-check" "octicon-x")}}</dd>
 			</dl>
 		</div>
 
@@ -200,7 +200,7 @@
 		<div class="ui attached table segment">
 			<dl class="admin-dl-horizontal">
 				<dt>{{ctx.Locale.Tr "admin.config.mailer_enabled"}}</dt>
-				<dd>{{if .MailerEnabled}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .MailerEnabled "octicon-check" "octicon-x")}}</dd>
 				{{if .MailerEnabled}}
 					<dt>{{ctx.Locale.Tr "admin.config.mailer_name"}}</dt>
 					<dd>{{.Mailer.Name}}</dd>
@@ -220,7 +220,7 @@
 						<dt>{{ctx.Locale.Tr "admin.config.mailer_protocol"}}</dt>
 						<dd>{{.Mailer.Protocol}}</dd>
 						<dt>{{ctx.Locale.Tr "admin.config.mailer_enable_helo"}}</dt>
-						<dd>{{if .Mailer.EnableHelo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+						<dd>{{svg (Iif .Mailer.EnableHelo "octicon-check" "octicon-x")}}</dd>
 						<dt>{{ctx.Locale.Tr "admin.config.mailer_smtp_addr"}}</dt>
 						<dd>{{.Mailer.SMTPAddr}}</dd>
 						<dt>{{ctx.Locale.Tr "admin.config.mailer_smtp_port"}}</dt>
@@ -279,7 +279,7 @@
 				<dt>{{ctx.Locale.Tr "admin.config.session_life_time"}}</dt>
 				<dd>{{.SessionConfig.Maxlifetime}} {{ctx.Locale.Tr "tool.raw_seconds"}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.https_only"}}</dt>
-				<dd>{{if .SessionConfig.Secure}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .SessionConfig.Secure "octicon-check" "octicon-x")}}</dd>
 			</dl>
 		</div>
 
@@ -289,7 +289,7 @@
 		<div class="ui attached table segment">
 			<dl class="admin-dl-horizontal">
 				<dt>{{ctx.Locale.Tr "admin.config.git_disable_diff_highlight"}}</dt>
-				<dd>{{if .Git.DisableDiffHighlight}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+				<dd>{{svg (Iif .Git.DisableDiffHighlight "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.git_max_diff_lines"}}</dt>
 				<dd>{{.Git.MaxGitDiffLines}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.git_max_diff_line_characters"}}</dt>
@@ -321,7 +321,7 @@
 			<dl class="admin-dl-horizontal">
 				{{if .Loggers.xorm.IsEnabled}}
 					<dt>{{ctx.Locale.Tr "admin.config.xorm_log_sql"}}</dt>
-					<dd>{{if $.LogSQL}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
+					<dd>{{svg (Iif $.LogSQL "octicon-check" "octicon-x")}}</dd>
 				{{end}}
 
 				{{if .Loggers.access.IsEnabled}}
diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl
index 3cb641488c..bb412ef146 100644
--- a/templates/admin/cron.tmpl
+++ b/templates/admin/cron.tmpl
@@ -26,7 +26,7 @@
 							<td>{{DateTime "full" .Next}}</td>
 							<td>{{if gt .Prev.Year 1}}{{DateTime "full" .Prev}}{{else}}-{{end}}</td>
 							<td>{{.ExecTimes}}</td>
-							<td {{if ne .Status ""}}data-tooltip-content="{{.FormatLastMessage ctx.Locale}}"{{end}} >{{if eq .Status ""}}—{{else if eq .Status "finished"}}{{svg "octicon-check" 16}}{{else}}{{svg "octicon-x" 16}}{{end}}</td>
+							<td {{if ne .Status ""}}data-tooltip-content="{{.FormatLastMessage ctx.Locale}}"{{end}} >{{if eq .Status ""}}—{{else}}{{svg (Iif (eq .Status "finished") "octicon-check" "octicon-x") 16}}{{end}}</td>
 						</tr>
 					{{end}}
 				</tbody>
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 388863df9b..1f226afcc4 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -46,17 +46,17 @@
 							<td><a href="{{AppSubUrl}}/{{.Name | PathEscape}}">{{.Name}}</a></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>{{svg (Iif .IsPrimary "octicon-check" "octicon-x")}}</td>
 							<td>
 								{{if .CanChange}}
 									<a class="link-email-action" href data-uid="{{.UID}}"
 										data-email="{{.Email}}"
 										data-primary="{{if .IsPrimary}}1{{else}}0{{end}}"
 										data-activate="{{if .IsActivated}}0{{else}}1{{end}}">
-										{{if .IsActivated}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+										{{svg (Iif .IsActivated "octicon-check" "octicon-x")}}
 									</a>
 								{{else}}
-									{{if .IsActivated}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+									{{svg (Iif .IsActivated "octicon-check" "octicon-x")}}
 								{{end}}
 							</td>
 						</tr>
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 528d047507..bc54d33431 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -93,9 +93,9 @@
 								{{end}}
 							</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>
+							<td>{{svg (Iif .IsActive "octicon-check" "octicon-x")}}</td>
+							<td>{{svg (Iif .IsRestricted "octicon-check" "octicon-x")}}</td>
+							<td>{{svg (Iif (index $.UsersTwoFaStatus .ID) "octicon-check" "octicon-x")}}</td>
 							<td>{{DateTime "short" .CreatedUnix}}</td>
 							{{if .LastLoginUnix}}
 								<td>{{DateTime "short" .LastLoginUnix}}</td>
diff --git a/templates/repo/diff/whitespace_dropdown.tmpl b/templates/repo/diff/whitespace_dropdown.tmpl
index c54de165a4..cf695791ca 100644
--- a/templates/repo/diff/whitespace_dropdown.tmpl
+++ b/templates/repo/diff/whitespace_dropdown.tmpl
@@ -27,4 +27,4 @@
 		</a>
 	</div>
 </div>
-<a class="ui tiny basic button" href="?style={{if .IsSplitStyle}}unified{{else}}split{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-tooltip-content="{{if .IsSplitStyle}}{{ctx.Locale.Tr "repo.diff.show_unified_view"}}{{else}}{{ctx.Locale.Tr "repo.diff.show_split_view"}}{{end}}">{{if .IsSplitStyle}}{{svg "gitea-join"}}{{else}}{{svg "gitea-split"}}{{end}}</a>
+<a class="ui tiny basic button" href="?style={{if .IsSplitStyle}}unified{{else}}split{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}" data-tooltip-content="{{if .IsSplitStyle}}{{ctx.Locale.Tr "repo.diff.show_unified_view"}}{{else}}{{ctx.Locale.Tr "repo.diff.show_split_view"}}{{end}}">{{svg (Iif .IsSplitStyle "gitea-join" "gitea-split")}}</a>
diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl
index 18986db773..88d0653f7d 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 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 .}}
+						{{if SliceUtils.Contains $.SelLabelIDs .ID}}{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}{{end}} {{RenderLabel $.Context ctx.Locale .}}
 						{{template "repo/issue/labels/label_archived" .}}
 					</div>
 				{{end}}
diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl
index e5f15caca5..3d65a7d8cd 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 ctx.Locale .}}
+				<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}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</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 ctx.Locale .}}
+				<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}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</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/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index bb0bb2cff3..ce34c5e939 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -92,7 +92,7 @@
 								</span>
 							{{end}}
 							{{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>
+								<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">{{svg (Iif .Checked "octicon-trash" "octicon-sync")}}</a>
 							{{end}}
 							{{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}}
 						</div>
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index 58d3759a9d..1243681f3a 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -36,7 +36,7 @@
 		{{if .HasMerged}}
 			<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>
+			<div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
 		{{else if .Issue.IsPull}}
 			{{if .IsPullWorkInProgress}}
 				<div class="ui grey label issue-state-label">{{svg "octicon-git-pull-request-draft"}} {{ctx.Locale.Tr "repo.issues.draft_title"}}</div>
diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl
index 758aec6bb0..4cfc0fc673 100644
--- a/templates/repo/settings/lfs_pointers.tmpl
+++ b/templates/repo/settings/lfs_pointers.tmpl
@@ -41,9 +41,9 @@
 									{{ShortSha .Oid}}
 								</a>
 							</td>
-							<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>{{svg (Iif .InRepo "octicon-check" "octicon-x")}}</td>
+							<td>{{svg (Iif .Exists "octicon-check" "octicon-x")}}</td>
+							<td>{{svg (Iif .Accessible "octicon-check" "octicon-x")}}</td>
 							<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>
diff --git a/templates/repo/star_unstar.tmpl b/templates/repo/star_unstar.tmpl
index 0f09d8b492..9234a0d196 100644
--- a/templates/repo/star_unstar.tmpl
+++ b/templates/repo/star_unstar.tmpl
@@ -3,7 +3,7 @@
 		{{$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}}
+			{{svg (Iif $.IsStaringRepo "octicon-star-fill" "octicon-star")}}
 			<span aria-hidden="true">{{$buttonText}}</span>
 		</button>
 		<a hx-boost="false" class="ui basic label" href="{{$.RepoLink}}/stars">

From 61c97fdef10d29f8813ee18734b37bb2797e3bab Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 11 Jun 2024 15:47:13 +0200
Subject: [PATCH 120/131] update nix flake and add gofumpt (#31320)

nix flake maintenance
---
 flake.lock | 6 +++---
 flake.nix  | 1 +
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/flake.lock b/flake.lock
index 0b2278f080..606f8836c1 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,11 +20,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1715534503,
-        "narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
+        "lastModified": 1717974879,
+        "narHash": "sha256-GTO3C88+5DX171F/gVS3Qga/hOs/eRMxPFpiHq2t+D8=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
+        "rev": "c7b821ba2e1e635ba5a76d299af62821cbcb09f3",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index c6e915e9db..22354663dd 100644
--- a/flake.nix
+++ b/flake.nix
@@ -30,6 +30,7 @@
 
             # backend
             go_1_22
+            gofumpt
           ];
         };
       }

From 4bf848a06bfa069e5f381235193924f2b35f2d9d Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 11 Jun 2024 22:52:12 +0800
Subject: [PATCH 121/131] Make template `Iif` exactly match `if` (#31322)

---
 modules/templates/helper.go      | 16 +++++-----
 modules/templates/helper_test.go | 52 ++++++++++++++++++++++++--------
 2 files changed, 47 insertions(+), 21 deletions(-)

diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 8779de69ca..330cbf8908 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -239,7 +239,7 @@ func DotEscape(raw string) string {
 // Iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version,
 // and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal).
 func Iif(condition any, vals ...any) any {
-	if IsTruthy(condition) {
+	if isTemplateTruthy(condition) {
 		return vals[0]
 	} else if len(vals) > 1 {
 		return vals[1]
@@ -247,7 +247,7 @@ func Iif(condition any, vals ...any) any {
 	return nil
 }
 
-func IsTruthy(v any) bool {
+func isTemplateTruthy(v any) bool {
 	if v == nil {
 		return false
 	}
@@ -256,20 +256,20 @@ func IsTruthy(v any) bool {
 	switch rv.Kind() {
 	case reflect.Bool:
 		return rv.Bool()
-	case reflect.String:
-		return rv.String() != ""
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		return rv.Int() != 0
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 		return rv.Uint() != 0
 	case reflect.Float32, reflect.Float64:
 		return rv.Float() != 0
-	case reflect.Slice, reflect.Array, reflect.Map:
+	case reflect.Complex64, reflect.Complex128:
+		return rv.Complex() != 0
+	case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
 		return rv.Len() > 0
-	case reflect.Ptr:
-		return !rv.IsNil() && IsTruthy(reflect.Indirect(rv).Interface())
+	case reflect.Struct:
+		return true
 	default:
-		return rv.Kind() == reflect.Struct && !rv.IsNil()
+		return !rv.IsNil()
 	}
 }
 
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index c6c70cc18e..ea5da7be80 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -5,8 +5,11 @@ package templates
 
 import (
 	"html/template"
+	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/modules/util"
+
 	"github.com/stretchr/testify/assert"
 )
 
@@ -66,17 +69,40 @@ 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>`))
 }
 
-func TestIsTruthy(t *testing.T) {
-	var test any
-	assert.Equal(t, false, IsTruthy(test))
-	assert.Equal(t, false, IsTruthy(nil))
-	assert.Equal(t, false, IsTruthy(""))
-	assert.Equal(t, true, IsTruthy("non-empty"))
-	assert.Equal(t, true, IsTruthy(-1))
-	assert.Equal(t, false, IsTruthy(0))
-	assert.Equal(t, true, IsTruthy(42))
-	assert.Equal(t, false, IsTruthy(0.0))
-	assert.Equal(t, true, IsTruthy(3.14))
-	assert.Equal(t, false, IsTruthy([]int{}))
-	assert.Equal(t, true, IsTruthy([]int{1}))
+func TestTemplateTruthy(t *testing.T) {
+	tmpl := template.New("test")
+	tmpl.Funcs(template.FuncMap{"Iif": Iif})
+	template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`))
+
+	cases := []any{
+		nil, false, true, "", "string", 0, 1,
+		byte(0), byte(1), int64(0), int64(1), float64(0), float64(1),
+		complex(0, 0), complex(1, 0),
+		(chan int)(nil), make(chan int),
+		(func())(nil), func() {},
+		util.ToPointer(0), util.ToPointer(util.ToPointer(0)),
+		util.ToPointer(1), util.ToPointer(util.ToPointer(1)),
+		[0]int{},
+		[1]int{0},
+		[]int(nil),
+		[]int{},
+		[]int{0},
+		map[any]any(nil),
+		map[any]any{},
+		map[any]any{"k": "v"},
+		(*struct{})(nil),
+		struct{}{},
+		util.ToPointer(struct{}{}),
+	}
+	w := &strings.Builder{}
+	truthyCount := 0
+	for i, v := range cases {
+		w.Reset()
+		assert.NoError(t, tmpl.Execute(w, struct{ Value any }{v}), "case %d (%T) %#v fails", i, v, v)
+		out := w.String()
+		truthyCount += util.Iif(out == "true:true", 1, 0)
+		truthyMatches := out == "true:true" || out == "false:false"
+		assert.True(t, truthyMatches, "case %d (%T) %#v fail: %s", i, v, v, out)
+	}
+	assert.True(t, truthyCount != 0 && truthyCount != len(cases))
 }

From fc2d75f86d77b022ece848acf2581c14ef21d43b Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Tue, 11 Jun 2024 20:47:45 +0200
Subject: [PATCH 122/131] Enable `unparam` linter (#31277)

Enable [unparam](https://github.com/mvdan/unparam) linter.

Often I could not tell the intention why param is unused, so I put
`//nolint` for those cases like webhook request creation functions never
using `ctx`.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
---
 .golangci.yml                          |  1 +
 models/dbfs/dbfile.go                  | 17 +++------
 models/issues/issue_search.go          | 51 ++++++++++----------------
 models/issues/pull_list.go             | 16 ++------
 modules/auth/password/hash/common.go   |  2 +-
 modules/packages/cran/metadata.go      | 18 ++++-----
 modules/setting/config_env.go          |  2 +-
 modules/setting/storage.go             |  2 +-
 modules/storage/azureblob.go           | 31 ++++------------
 modules/util/keypair.go                |  9 ++---
 routers/api/actions/artifacts.go       |  8 +---
 routers/api/actions/artifacts_utils.go |  8 ++--
 routers/api/packages/container/blob.go |  2 +-
 routers/api/packages/nuget/nuget.go    |  2 +-
 routers/api/v1/repo/compare.go         |  2 +-
 routers/api/v1/repo/pull.go            | 28 +++++++-------
 routers/web/admin/config.go            |  2 +-
 services/pull/merge.go                 |  2 +-
 services/webhook/dingtalk.go           |  2 +-
 services/webhook/discord.go            |  2 +-
 services/webhook/feishu.go             |  2 +-
 services/webhook/matrix.go             |  2 +-
 services/webhook/msteams.go            |  2 +-
 services/webhook/packagist.go          |  2 +-
 services/webhook/slack.go              |  2 +-
 services/webhook/telegram.go           |  2 +-
 services/webhook/wechatwork.go         |  2 +-
 27 files changed, 86 insertions(+), 135 deletions(-)

diff --git a/.golangci.yml b/.golangci.yml
index 1750872765..37617ad365 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -22,6 +22,7 @@ linters:
     - typecheck
     - unconvert
     - unused
+    - unparam
     - wastedassign
 
 run:
diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go
index 3650ce057e..dd27b5c36b 100644
--- a/models/dbfs/dbfile.go
+++ b/models/dbfs/dbfile.go
@@ -215,16 +215,15 @@ func fileTimestampToTime(timestamp int64) time.Time {
 	return time.UnixMicro(timestamp)
 }
 
-func (f *file) loadMetaByPath() (*dbfsMeta, error) {
+func (f *file) loadMetaByPath() error {
 	var fileMeta dbfsMeta
 	if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil {
-		return nil, err
+		return err
 	} else if ok {
 		f.metaID = fileMeta.ID
 		f.blockSize = fileMeta.BlockSize
-		return &fileMeta, nil
 	}
-	return nil, nil
+	return nil
 }
 
 func (f *file) open(flag int) (err error) {
@@ -288,10 +287,7 @@ func (f *file) createEmpty() error {
 	if err != nil {
 		return err
 	}
-	if _, err = f.loadMetaByPath(); err != nil {
-		return err
-	}
-	return nil
+	return f.loadMetaByPath()
 }
 
 func (f *file) truncate() error {
@@ -368,8 +364,5 @@ func buildPath(path string) string {
 func newDbFile(ctx context.Context, path string) (*file, error) {
 	path = buildPath(path)
 	f := &file{ctx: ctx, fullPath: path, blockSize: defaultFileBlockSize}
-	if _, err := f.loadMetaByPath(); err != nil {
-		return nil, err
-	}
-	return f, nil
+	return f, f.loadMetaByPath()
 }
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index 491def1229..c1d7d921a9 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -99,9 +99,9 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
 	}
 }
 
-func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+func applyLimit(sess *xorm.Session, opts *IssuesOptions) {
 	if opts.Paginator == nil || opts.Paginator.IsListAll() {
-		return sess
+		return
 	}
 
 	start := 0
@@ -109,11 +109,9 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 		start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize
 	}
 	sess.Limit(opts.Paginator.PageSize, start)
-
-	return sess
 }
 
-func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) {
 	if len(opts.LabelIDs) > 0 {
 		if opts.LabelIDs[0] == 0 {
 			sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)")
@@ -136,11 +134,9 @@ func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
 	if len(opts.ExcludedLabelNames) > 0 {
 		sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames)))
 	}
-
-	return sess
 }
 
-func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) {
 	if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID {
 		sess.And("issue.milestone_id = 0")
 	} else if len(opts.MilestoneIDs) > 0 {
@@ -153,11 +149,9 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sess
 				From("milestone").
 				Where(builder.In("name", opts.IncludeMilestones)))
 	}
-
-	return sess
 }
 
-func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) {
 	if opts.ProjectID > 0 { // specific project
 		sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
 			And("project_issue.project_id=?", opts.ProjectID)
@@ -166,10 +160,9 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
 	}
 	// opts.ProjectID == 0 means all projects,
 	// do not need to apply any condition
-	return sess
 }
 
-func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) {
 	// opts.ProjectColumnID == 0 means all project columns,
 	// do not need to apply any condition
 	if opts.ProjectColumnID > 0 {
@@ -177,10 +170,9 @@ func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.
 	} else if opts.ProjectColumnID == db.NoConditionID {
 		sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
 	}
-	return sess
 }
 
-func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) {
 	if len(opts.RepoIDs) == 1 {
 		opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]}
 	} else if len(opts.RepoIDs) > 1 {
@@ -195,10 +187,9 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
 	if opts.RepoCond != nil {
 		sess.And(opts.RepoCond)
 	}
-	return sess
 }
 
-func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
 	if len(opts.IssueIDs) > 0 {
 		sess.In("issue.id", opts.IssueIDs)
 	}
@@ -261,8 +252,6 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
 	if opts.User != nil {
 		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
 	}
-
-	return sess
 }
 
 // teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access
@@ -339,22 +328,22 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati
 	return cond
 }
 
-func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session {
-	return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
+func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) {
+	sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
 		And("issue_assignees.assignee_id = ?", assigneeID)
 }
 
-func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session {
-	return sess.And("issue.poster_id=?", posterID)
+func applyPosterCondition(sess *xorm.Session, posterID int64) {
+	sess.And("issue.poster_id=?", posterID)
 }
 
-func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session {
-	return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
+func applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
+	sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
 		And("issue_user.is_mentioned = ?", true).
 		And("issue_user.uid = ?", mentionedID)
 }
 
-func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session {
+func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) {
 	existInTeamQuery := builder.Select("team_user.team_id").
 		From("team_user").
 		Where(builder.Eq{"team_user.uid": reviewRequestedID})
@@ -375,11 +364,11 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
 			),
 			builder.In("review.id", maxReview),
 		))
-	return sess.Where("issue.poster_id <> ?", reviewRequestedID).
+	sess.Where("issue.poster_id <> ?", reviewRequestedID).
 		And(builder.In("issue.id", subQuery))
 }
 
-func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session {
+func applyReviewedCondition(sess *xorm.Session, reviewedID int64) {
 	// Query for pull requests where you are a reviewer or commenter, excluding
 	// any pull requests already returned by the review requested filter.
 	notPoster := builder.Neq{"issue.poster_id": reviewedID}
@@ -406,11 +395,11 @@ func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session
 			builder.In("type", CommentTypeComment, CommentTypeCode, CommentTypeReview),
 		)),
 	)
-	return sess.And(notPoster, builder.Or(reviewed, commented))
+	sess.And(notPoster, builder.Or(reviewed, commented))
 }
 
-func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session {
-	return sess.And(
+func applySubscribedCondition(sess *xorm.Session, subscriberID int64) {
+	sess.And(
 		builder.
 			NotIn("issue.id",
 				builder.Select("issue_id").
diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go
index e8011a916f..a1d46f8cd4 100644
--- a/models/issues/pull_list.go
+++ b/models/issues/pull_list.go
@@ -28,7 +28,7 @@ type PullRequestsOptions struct {
 	MilestoneID int64
 }
 
-func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (*xorm.Session, error) {
+func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
 	sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID)
 
 	sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
@@ -46,7 +46,7 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
 		sess.And("issue.milestone_id=?", opts.MilestoneID)
 	}
 
-	return sess, nil
+	return sess
 }
 
 // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
@@ -130,23 +130,15 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
 		opts.Page = 1
 	}
 
-	countSession, err := listPullRequestStatement(ctx, baseRepoID, opts)
-	if err != nil {
-		log.Error("listPullRequestStatement: %v", err)
-		return nil, 0, err
-	}
+	countSession := listPullRequestStatement(ctx, baseRepoID, opts)
 	maxResults, err := countSession.Count(new(PullRequest))
 	if err != nil {
 		log.Error("Count PRs: %v", err)
 		return nil, maxResults, err
 	}
 
-	findSession, err := listPullRequestStatement(ctx, baseRepoID, opts)
+	findSession := listPullRequestStatement(ctx, baseRepoID, opts)
 	applySorts(findSession, opts.SortType, 0)
-	if err != nil {
-		log.Error("listPullRequestStatement: %v", err)
-		return nil, maxResults, err
-	}
 	findSession = db.SetSessionPagination(findSession, opts)
 	prs := make([]*PullRequest, 0, opts.PageSize)
 	return prs, maxResults, findSession.Find(&prs)
diff --git a/modules/auth/password/hash/common.go b/modules/auth/password/hash/common.go
index ac6faf35cf..487c0738f4 100644
--- a/modules/auth/password/hash/common.go
+++ b/modules/auth/password/hash/common.go
@@ -18,7 +18,7 @@ func parseIntParam(value, param, algorithmName, config string, previousErr error
 	return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed
 }
 
-func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) {
+func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam
 	parsed, err := strconv.ParseUint(value, 10, 64)
 	if err != nil {
 		log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)
diff --git a/modules/packages/cran/metadata.go b/modules/packages/cran/metadata.go
index 24e6f323af..0b0bfb07c6 100644
--- a/modules/packages/cran/metadata.go
+++ b/modules/packages/cran/metadata.go
@@ -185,8 +185,6 @@ func ParseDescription(r io.Reader) (*Package, error) {
 }
 
 func setField(p *Package, data string) error {
-	const listDelimiter = ", "
-
 	if data == "" {
 		return nil
 	}
@@ -215,19 +213,19 @@ func setField(p *Package, data string) error {
 	case "Description":
 		p.Metadata.Description = value
 	case "URL":
-		p.Metadata.ProjectURL = splitAndTrim(value, listDelimiter)
+		p.Metadata.ProjectURL = splitAndTrim(value)
 	case "License":
 		p.Metadata.License = value
 	case "Author":
-		p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""), listDelimiter)
+		p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
 	case "Depends":
-		p.Metadata.Depends = splitAndTrim(value, listDelimiter)
+		p.Metadata.Depends = splitAndTrim(value)
 	case "Imports":
-		p.Metadata.Imports = splitAndTrim(value, listDelimiter)
+		p.Metadata.Imports = splitAndTrim(value)
 	case "Suggests":
-		p.Metadata.Suggests = splitAndTrim(value, listDelimiter)
+		p.Metadata.Suggests = splitAndTrim(value)
 	case "LinkingTo":
-		p.Metadata.LinkingTo = splitAndTrim(value, listDelimiter)
+		p.Metadata.LinkingTo = splitAndTrim(value)
 	case "NeedsCompilation":
 		p.Metadata.NeedsCompilation = value == "yes"
 	}
@@ -235,8 +233,8 @@ func setField(p *Package, data string) error {
 	return nil
 }
 
-func splitAndTrim(s, sep string) []string {
-	items := strings.Split(s, sep)
+func splitAndTrim(s string) []string {
+	items := strings.Split(s, ", ")
 	for i := range items {
 		items[i] = strings.TrimSpace(items[i])
 	}
diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go
index 242f40914a..dfcb7db3c8 100644
--- a/modules/setting/config_env.go
+++ b/modules/setting/config_env.go
@@ -97,7 +97,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
 
 // decodeEnvironmentKey decode the environment key to section and key
 // The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
-func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
+func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) { //nolint:unparam
 	if !strings.HasPrefix(envKey, prefixGitea) {
 		return false, "", "", false
 	}
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index d44c968423..d6f7672b61 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -161,7 +161,7 @@ const (
 	targetSecIsSec                                  // target section is from the name seciont [name]
 )
 
-func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) {
+func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam
 	targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ)
 	if err != nil {
 		if !IsValidStorageType(StorageType(typ)) {
diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go
index 52a7d1637e..211522c5bb 100644
--- a/modules/storage/azureblob.go
+++ b/modules/storage/azureblob.go
@@ -163,10 +163,7 @@ func (a *AzureBlobStorage) getObjectNameFromPath(path string) string {
 
 // Open opens a file
 func (a *AzureBlobStorage) Open(path string) (Object, error) {
-	blobClient, err := a.getBlobClient(path)
-	if err != nil {
-		return nil, convertAzureBlobErr(err)
-	}
+	blobClient := a.getBlobClient(path)
 	res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
 	if err != nil {
 		return nil, convertAzureBlobErr(err)
@@ -229,10 +226,7 @@ func (a azureBlobFileInfo) Sys() any {
 
 // Stat returns the stat information of the object
 func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) {
-	blobClient, err := a.getBlobClient(path)
-	if err != nil {
-		return nil, convertAzureBlobErr(err)
-	}
+	blobClient := a.getBlobClient(path)
 	res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
 	if err != nil {
 		return nil, convertAzureBlobErr(err)
@@ -247,20 +241,14 @@ func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) {
 
 // Delete delete a file
 func (a *AzureBlobStorage) Delete(path string) error {
-	blobClient, err := a.getBlobClient(path)
-	if err != nil {
-		return convertAzureBlobErr(err)
-	}
-	_, err = blobClient.Delete(a.ctx, nil)
+	blobClient := a.getBlobClient(path)
+	_, err := blobClient.Delete(a.ctx, nil)
 	return convertAzureBlobErr(err)
 }
 
 // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
 func (a *AzureBlobStorage) URL(path, name string) (*url.URL, error) {
-	blobClient, err := a.getBlobClient(path)
-	if err != nil {
-		return nil, convertAzureBlobErr(err)
-	}
+	blobClient := a.getBlobClient(path)
 
 	startTime := time.Now()
 	u, err := blobClient.GetSASURL(sas.BlobPermissions{
@@ -290,10 +278,7 @@ func (a *AzureBlobStorage) IterateObjects(dirName string, fn func(path string, o
 			return convertAzureBlobErr(err)
 		}
 		for _, object := range resp.Segment.BlobItems {
-			blobClient, err := a.getBlobClient(*object.Name)
-			if err != nil {
-				return convertAzureBlobErr(err)
-			}
+			blobClient := a.getBlobClient(*object.Name)
 			object := &azureBlobObject{
 				Context:    a.ctx,
 				blobClient: blobClient,
@@ -313,8 +298,8 @@ func (a *AzureBlobStorage) IterateObjects(dirName string, fn func(path string, o
 }
 
 // Delete delete a file
-func (a *AzureBlobStorage) getBlobClient(path string) (*blob.Client, error) {
-	return a.client.ServiceClient().NewContainerClient(a.cfg.Container).NewBlobClient(a.buildAzureBlobPath(path)), nil
+func (a *AzureBlobStorage) getBlobClient(path string) *blob.Client {
+	return a.client.ServiceClient().NewContainerClient(a.cfg.Container).NewBlobClient(a.buildAzureBlobPath(path))
 }
 
 func init() {
diff --git a/modules/util/keypair.go b/modules/util/keypair.go
index 8b86c142af..07f27bd1ba 100644
--- a/modules/util/keypair.go
+++ b/modules/util/keypair.go
@@ -15,10 +15,7 @@ import (
 // GenerateKeyPair generates a public and private keypair
 func GenerateKeyPair(bits int) (string, string, error) {
 	priv, _ := rsa.GenerateKey(rand.Reader, bits)
-	privPem, err := pemBlockForPriv(priv)
-	if err != nil {
-		return "", "", err
-	}
+	privPem := pemBlockForPriv(priv)
 	pubPem, err := pemBlockForPub(&priv.PublicKey)
 	if err != nil {
 		return "", "", err
@@ -26,12 +23,12 @@ func GenerateKeyPair(bits int) (string, string, error) {
 	return privPem, pubPem, nil
 }
 
-func pemBlockForPriv(priv *rsa.PrivateKey) (string, error) {
+func pemBlockForPriv(priv *rsa.PrivateKey) string {
 	privBytes := pem.EncodeToMemory(&pem.Block{
 		Type:  "RSA PRIVATE KEY",
 		Bytes: x509.MarshalPKCS1PrivateKey(priv),
 	})
-	return string(privBytes), nil
+	return string(privBytes)
 }
 
 func pemBlockForPub(pub *rsa.PublicKey) (string, error) {
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
index 16af957d0f..72a2a26c47 100644
--- a/routers/api/actions/artifacts.go
+++ b/routers/api/actions/artifacts.go
@@ -242,16 +242,12 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
 	}
 
 	// get upload file size
-	fileRealTotalSize, contentLength, err := getUploadFileSize(ctx)
-	if err != nil {
-		log.Error("Error get upload file size: %v", err)
-		ctx.Error(http.StatusInternalServerError, "Error get upload file size")
-		return
-	}
+	fileRealTotalSize, contentLength := getUploadFileSize(ctx)
 
 	// get artifact retention days
 	expiredDays := setting.Actions.ArtifactRetentionDays
 	if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" {
+		var err error
 		expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64)
 		if err != nil {
 			log.Error("Error parse retention days: %v", err)
diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go
index aaf89ef40e..3517d57f78 100644
--- a/routers/api/actions/artifacts_utils.go
+++ b/routers/api/actions/artifacts_utils.go
@@ -43,7 +43,7 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
 	return task, runID, true
 }
 
-func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
+func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam
 	task := ctx.ActionTask
 	runID, err := strconv.ParseInt(rawRunID, 10, 64)
 	if err != nil || task.Job.RunID != runID {
@@ -84,11 +84,11 @@ func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
 
 // getUploadFileSize returns the size of the file to be uploaded.
 // The raw size is the size of the file as reported by the header X-TFS-FileLength.
-func getUploadFileSize(ctx *ArtifactContext) (int64, int64, error) {
+func getUploadFileSize(ctx *ArtifactContext) (int64, int64) {
 	contentLength := ctx.Req.ContentLength
 	xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64)
 	if xTfsLength > 0 {
-		return xTfsLength, contentLength, nil
+		return xTfsLength, contentLength
 	}
-	return contentLength, contentLength, nil
+	return contentLength, contentLength
 }
diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go
index f2d63297c1..9e3a47076c 100644
--- a/routers/api/packages/container/blob.go
+++ b/routers/api/packages/container/blob.go
@@ -26,7 +26,7 @@ var uploadVersionMutex sync.Mutex
 
 // saveAsPackageBlob creates a package blob from an upload
 // The uploaded blob gets stored in a special upload version to link them to the package/image
-func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) {
+func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam
 	pb := packages_service.NewPackageBlob(hsr)
 
 	exists := false
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index 3633d0d007..0d7212d7f7 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -36,7 +36,7 @@ func apiError(ctx *context.Context, status int, obj any) {
 	})
 }
 
-func xmlResponse(ctx *context.Context, status int, obj any) {
+func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam
 	ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
 	ctx.Resp.WriteHeader(status)
 	if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil {
diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go
index cfd61d768c..429145c714 100644
--- a/routers/api/v1/repo/compare.go
+++ b/routers/api/v1/repo/compare.go
@@ -64,7 +64,7 @@ func CompareDiff(ctx *context.APIContext) {
 		}
 	}
 
-	_, _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{
+	_, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{
 		Base: infos[0],
 		Head: infos[1],
 	})
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 4014fe80f3..1fc94708da 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -408,7 +408,7 @@ func CreatePullRequest(ctx *context.APIContext) {
 	)
 
 	// Get repo/branch information
-	_, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
+	headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
 	if ctx.Written() {
 		return
 	}
@@ -1054,7 +1054,7 @@ func MergePullRequest(ctx *context.APIContext) {
 	ctx.Status(http.StatusOK)
 }
 
-func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*user_model.User, *repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) {
+func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) {
 	baseRepo := ctx.Repo.Repository
 
 	// Get compared branches information
@@ -1087,14 +1087,14 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 			} else {
 				ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
 			}
-			return nil, nil, nil, nil, "", ""
+			return 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()
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 
 	ctx.Repo.PullRequest.SameRepo = isSameRepo
@@ -1102,7 +1102,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 	// Check if base branch is valid.
 	if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) {
 		ctx.NotFound("BaseNotExist")
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 
 	// Check if current user has fork of repository or in the same repository.
@@ -1110,7 +1110,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 	if headRepo == nil && !isSameRepo {
 		log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
 		ctx.NotFound("GetForkedRepo")
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 
 	var headGitRepo *git.Repository
@@ -1121,7 +1121,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 		headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
-			return nil, nil, nil, nil, "", ""
+			return nil, nil, nil, "", ""
 		}
 	}
 
@@ -1130,7 +1130,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 	if err != nil {
 		headGitRepo.Close()
 		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 	if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
 		if log.IsTrace() {
@@ -1141,7 +1141,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 		}
 		headGitRepo.Close()
 		ctx.NotFound("Can't read pulls or can't read UnitTypeCode")
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 
 	// user should have permission to read headrepo's codes
@@ -1149,7 +1149,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 	if err != nil {
 		headGitRepo.Close()
 		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 	if !permHead.CanRead(unit.TypeCode) {
 		if log.IsTrace() {
@@ -1160,24 +1160,24 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
 		}
 		headGitRepo.Close()
 		ctx.NotFound("Can't read headRepo UnitTypeCode")
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 
 	// Check if head branch is valid.
 	if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) {
 		headGitRepo.Close()
 		ctx.NotFound()
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 
 	compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false)
 	if err != nil {
 		headGitRepo.Close()
 		ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
-		return nil, nil, nil, nil, "", ""
+		return nil, nil, nil, "", ""
 	}
 
-	return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch
+	return headRepo, headGitRepo, compareInfo, baseBranch, headBranch
 }
 
 // UpdatePullRequest merge PR's baseBranch into headBranch
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index fd8c73b62d..2a842cff82 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -183,7 +183,7 @@ func ChangeConfig(ctx *context.Context) {
 	value := ctx.FormString("value")
 	cfg := setting.Config()
 
-	marshalBool := func(v string) (string, error) {
+	marshalBool := func(v string) (string, error) { //nolint:unparam
 		if b, _ := strconv.ParseBool(v); b {
 			return "true", nil
 		}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 6b5e9ea330..9ef3fb2e05 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -246,7 +246,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
 }
 
 // doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository
-func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) {
+func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) { //nolint:unparam
 	// Clone base repo.
 	mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID)
 	if err != nil {
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index c57d04415a..f6018f7374 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -190,6 +190,6 @@ type dingtalkConvertor struct{}
 
 var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
 
-func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newDingtalkRequest(_ 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/discord.go b/services/webhook/discord.go
index 3883ac9eb8..31332396f2 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -260,7 +260,7 @@ type discordConvertor struct {
 
 var _ payloadConvertor[DiscordPayload] = discordConvertor{}
 
-func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newDiscordRequest(_ 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)
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 1ec436894b..38f324aa7b 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -168,6 +168,6 @@ type feishuConvertor struct{}
 
 var _ payloadConvertor[FeishuPayload] = feishuConvertor{}
 
-func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newFeishuRequest(_ 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/matrix.go b/services/webhook/matrix.go
index 5dcfdcb0dd..e649a07609 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -24,7 +24,7 @@ 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) {
+func newMatrixRequest(_ 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)
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 99d0106184..b052b9da10 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -347,6 +347,6 @@ type msteamsConvertor struct{}
 
 var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
 
-func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newMSTeamsRequest(_ 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/packagist.go b/services/webhook/packagist.go
index 7880d8b606..593b97a174 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -112,7 +112,7 @@ type packagistConvertor struct {
 
 var _ payloadConvertor[PackagistPayload] = packagistConvertor{}
 
-func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newPackagistRequest(_ 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)
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index ba8bac27d9..ffa2936bea 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -283,7 +283,7 @@ type slackConvertor struct {
 
 var _ payloadConvertor[SlackPayload] = slackConvertor{}
 
-func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newSlackRequest(_ 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)
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index c2b4820032..de6c878dad 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -191,6 +191,6 @@ type telegramConvertor struct{}
 
 var _ payloadConvertor[TelegramPayload] = telegramConvertor{}
 
-func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newTelegramRequest(_ 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/wechatwork.go b/services/webhook/wechatwork.go
index 46e7856ecf..2e9d31cb7c 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -177,6 +177,6 @@ type wechatworkConvertor struct{}
 
 var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
 
-func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	return newJSONRequest(wechatworkConvertor{}, w, t, true)
 }

From e25d6960b5749fbf7f88ebb6b27878c0459817da Mon Sep 17 00:00:00 2001
From: Zoupers Zou <1171443643@qq.com>
Date: Wed, 12 Jun 2024 06:22:28 +0800
Subject: [PATCH 123/131] Fix #31185 try fix lfs download from bitbucket failed
 (#31201)

Fix #31185
---
 modules/lfs/http_client.go                   |  4 ++--
 modules/lfs/http_client_test.go              |  4 ++--
 modules/lfs/shared.go                        |  2 ++
 modules/lfs/transferadapter.go               |  1 +
 modules/lfs/transferadapter_test.go          |  2 +-
 services/lfs/server.go                       |  2 +-
 tests/integration/api_repo_lfs_locks_test.go | 10 +++++-----
 tests/integration/api_repo_lfs_test.go       |  4 ++--
 8 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go
index e06879baea..f5ddd38b09 100644
--- a/modules/lfs/http_client.go
+++ b/modules/lfs/http_client.go
@@ -211,7 +211,7 @@ func createRequest(ctx context.Context, method, url string, headers map[string]s
 	for key, value := range headers {
 		req.Header.Set(key, value)
 	}
-	req.Header.Set("Accept", MediaType)
+	req.Header.Set("Accept", AcceptHeader)
 
 	return req, nil
 }
@@ -251,6 +251,6 @@ func handleErrorResponse(resp *http.Response) error {
 		return err
 	}
 
-	log.Trace("ErrorResponse: %v", er)
+	log.Trace("ErrorResponse(%v): %v", resp.Status, er)
 	return errors.New(er.Message)
 }
diff --git a/modules/lfs/http_client_test.go b/modules/lfs/http_client_test.go
index 7459d9c0c9..7431132f76 100644
--- a/modules/lfs/http_client_test.go
+++ b/modules/lfs/http_client_test.go
@@ -155,7 +155,7 @@ func TestHTTPClientDownload(t *testing.T) {
 	hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
 		assert.Equal(t, "POST", req.Method)
 		assert.Equal(t, MediaType, req.Header.Get("Content-type"))
-		assert.Equal(t, MediaType, req.Header.Get("Accept"))
+		assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
 
 		var batchRequest BatchRequest
 		err := json.NewDecoder(req.Body).Decode(&batchRequest)
@@ -263,7 +263,7 @@ func TestHTTPClientUpload(t *testing.T) {
 	hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
 		assert.Equal(t, "POST", req.Method)
 		assert.Equal(t, MediaType, req.Header.Get("Content-type"))
-		assert.Equal(t, MediaType, req.Header.Get("Accept"))
+		assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
 
 		var batchRequest BatchRequest
 		err := json.NewDecoder(req.Body).Decode(&batchRequest)
diff --git a/modules/lfs/shared.go b/modules/lfs/shared.go
index 6b2e55f2fb..80f4fed00d 100644
--- a/modules/lfs/shared.go
+++ b/modules/lfs/shared.go
@@ -10,6 +10,8 @@ import (
 const (
 	// MediaType contains the media type for LFS server requests
 	MediaType = "application/vnd.git-lfs+json"
+	// Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served
+	AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8"
 )
 
 // BatchRequest contains multiple requests processed in one batch operation.
diff --git a/modules/lfs/transferadapter.go b/modules/lfs/transferadapter.go
index d425b91946..fbc3a3ad8c 100644
--- a/modules/lfs/transferadapter.go
+++ b/modules/lfs/transferadapter.go
@@ -37,6 +37,7 @@ func (a *BasicTransferAdapter) Download(ctx context.Context, l *Link) (io.ReadCl
 	if err != nil {
 		return nil, err
 	}
+	log.Debug("Download Request: %+v", req)
 	resp, err := performRequest(ctx, a.client, req)
 	if err != nil {
 		return nil, err
diff --git a/modules/lfs/transferadapter_test.go b/modules/lfs/transferadapter_test.go
index 6023cd07d3..7fec137efe 100644
--- a/modules/lfs/transferadapter_test.go
+++ b/modules/lfs/transferadapter_test.go
@@ -26,7 +26,7 @@ func TestBasicTransferAdapter(t *testing.T) {
 	p := Pointer{Oid: "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", Size: 5}
 
 	roundTripHandler := func(req *http.Request) *http.Response {
-		assert.Equal(t, MediaType, req.Header.Get("Accept"))
+		assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
 		assert.Equal(t, "test-value", req.Header.Get("test-header"))
 
 		url := req.URL.String()
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 2e330aa1a4..ae3dffe0c2 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -477,7 +477,7 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa
 			}
 
 			// This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
-			verifyHeader["Accept"] = lfs_module.MediaType
+			verifyHeader["Accept"] = lfs_module.AcceptHeader
 
 			rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader}
 		}
diff --git a/tests/integration/api_repo_lfs_locks_test.go b/tests/integration/api_repo_lfs_locks_test.go
index 5aa1396941..427e0b9fb1 100644
--- a/tests/integration/api_repo_lfs_locks_test.go
+++ b/tests/integration/api_repo_lfs_locks_test.go
@@ -105,7 +105,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
 	for _, test := range tests {
 		session := loginUser(t, test.user.Name)
 		req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks", test.repo.FullName()), map[string]string{"path": test.path})
-		req.Header.Set("Accept", lfs.MediaType)
+		req.Header.Set("Accept", lfs.AcceptHeader)
 		req.Header.Set("Content-Type", lfs.MediaType)
 		resp := session.MakeRequest(t, req, test.httpResult)
 		if len(test.addTime) > 0 {
@@ -123,7 +123,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
 	for _, test := range resultsTests {
 		session := loginUser(t, test.user.Name)
 		req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
-		req.Header.Set("Accept", lfs.MediaType)
+		req.Header.Set("Accept", lfs.AcceptHeader)
 		resp := session.MakeRequest(t, req, http.StatusOK)
 		var lfsLocks api.LFSLockList
 		DecodeJSON(t, resp, &lfsLocks)
@@ -135,7 +135,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
 		}
 
 		req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/verify", test.repo.FullName()), map[string]string{})
-		req.Header.Set("Accept", lfs.MediaType)
+		req.Header.Set("Accept", lfs.AcceptHeader)
 		req.Header.Set("Content-Type", lfs.MediaType)
 		resp = session.MakeRequest(t, req, http.StatusOK)
 		var lfsLocksVerify api.LFSLockListVerify
@@ -159,7 +159,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
 	for _, test := range deleteTests {
 		session := loginUser(t, test.user.Name)
 		req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/%s/unlock", test.repo.FullName(), test.lockID), map[string]string{})
-		req.Header.Set("Accept", lfs.MediaType)
+		req.Header.Set("Accept", lfs.AcceptHeader)
 		req.Header.Set("Content-Type", lfs.MediaType)
 		resp := session.MakeRequest(t, req, http.StatusOK)
 		var lfsLockRep api.LFSLockResponse
@@ -172,7 +172,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
 	for _, test := range resultsTests {
 		session := loginUser(t, test.user.Name)
 		req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
-		req.Header.Set("Accept", lfs.MediaType)
+		req.Header.Set("Accept", lfs.AcceptHeader)
 		resp := session.MakeRequest(t, req, http.StatusOK)
 		var lfsLocks api.LFSLockList
 		DecodeJSON(t, resp, &lfsLocks)
diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go
index 211dcf76c1..6b42b83bc5 100644
--- a/tests/integration/api_repo_lfs_test.go
+++ b/tests/integration/api_repo_lfs_test.go
@@ -84,7 +84,7 @@ func TestAPILFSBatch(t *testing.T) {
 
 	newRequest := func(t testing.TB, br *lfs.BatchRequest) *RequestWrapper {
 		return NewRequestWithJSON(t, "POST", "/user2/lfs-batch-repo.git/info/lfs/objects/batch", br).
-			SetHeader("Accept", lfs.MediaType).
+			SetHeader("Accept", lfs.AcceptHeader).
 			SetHeader("Content-Type", lfs.MediaType)
 	}
 	decodeResponse := func(t *testing.T, b *bytes.Buffer) *lfs.BatchResponse {
@@ -447,7 +447,7 @@ func TestAPILFSVerify(t *testing.T) {
 
 	newRequest := func(t testing.TB, p *lfs.Pointer) *RequestWrapper {
 		return NewRequestWithJSON(t, "POST", "/user2/lfs-verify-repo.git/info/lfs/verify", p).
-			SetHeader("Accept", lfs.MediaType).
+			SetHeader("Accept", lfs.AcceptHeader).
 			SetHeader("Content-Type", lfs.MediaType)
 	}
 

From a975ce8d9db773b060b7233e91fd490b6a6bfe46 Mon Sep 17 00:00:00 2001
From: Kerwin Bryant <kerwin612@qq.com>
Date: Wed, 12 Jun 2024 12:06:12 +0800
Subject: [PATCH 124/131] Optimize profile layout to enhance visual experience
 (#31278)

Co-authored-by: silverwind <me@silverwind.io>
---
 templates/shared/user/profile_big_avatar.tmpl | 12 ++----------
 1 file changed, 2 insertions(+), 10 deletions(-)

diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 29c6eb0eb0..1069209495 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -48,16 +48,8 @@
 				<li>
 					{{svg "octicon-mail"}}
 					<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"}}">
-								{{svg "octicon-unlock"}}
-							</i>
-						{{else}}
-							<i data-tooltip-content="{{ctx.Locale.Tr "user.email_visibility.private"}}">
-								{{svg "octicon-lock"}}
-							</i>
-						{{end}}
+					<a class="flex-text-inline" href="{{AppSubUrl}}/user/settings#privacy-user-settings" data-tooltip-content="{{ctx.Locale.Tr (Iif .ShowUserEmail "user.email_visibility.limited" "user.email_visibility.private")}}">
+						{{svg (Iif .ShowUserEmail "octicon-unlock" "octicon-lock")}}
 					</a>
 				</li>
 			{{else}}

From 1968c2222dcf47ebd1697afb4e79a81e74702d31 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 12 Jun 2024 18:22:01 +0800
Subject: [PATCH 125/131] Fix adopt repository has empty object name in
 database (#31333)

Fix #31330
Fix #31311

A workaround to fix the old database is to update object_format_name to
`sha1` if it's empty or null.
---
 modules/repository/branch.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/repository/branch.go b/modules/repository/branch.go
index a3fca7c7ce..2bf9930f19 100644
--- a/modules/repository/branch.go
+++ b/modules/repository/branch.go
@@ -45,6 +45,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
 	if err != nil {
 		return 0, fmt.Errorf("UpdateRepository: %w", err)
 	}
+	repo.ObjectFormatName = objFmt.Name() // keep consistent with db
 
 	allBranches := container.Set[string]{}
 	{

From 130ea31d6d4105d466185759186c5ece5148731f Mon Sep 17 00:00:00 2001
From: Yarden Shoham <git@yardenshoham.com>
Date: Wed, 12 Jun 2024 13:27:00 +0300
Subject: [PATCH 126/131] Fix dates displaying in a wrong manner when we're
 close to the end of the month (#31331)

I tested and all timestamps work as before.

- Reference https://github.com/github/relative-time-element/pull/285
- Fixes https://github.com/go-gitea/gitea/issues/31197

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 8b1ba766d5..56c6f8643e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
         "@citation-js/plugin-csl": "0.7.11",
         "@citation-js/plugin-software-formats": "0.6.1",
         "@github/markdown-toolbar-element": "2.2.3",
-        "@github/relative-time-element": "4.4.1",
+        "@github/relative-time-element": "4.4.2",
         "@github/text-expander-element": "2.6.1",
         "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
         "@primer/octicons": "19.9.0",
@@ -1028,9 +1028,9 @@
       "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
     },
     "node_modules/@github/relative-time-element": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.1.tgz",
-      "integrity": "sha512-E2vRcIgDj8AHv/iHpQMLJ/RqKOJ704OXkKw6+Zdhk3X+kVQhOf3Wj8KVz4DfCQ1eOJR8XxY6XVv73yd+pjMfXA=="
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.2.tgz",
+      "integrity": "sha512-wTXunu3hmuGljA5CHaaoUIKV0oI35wno0FKJl2yqKplTRnsCA5bPNj4bDeVIubkuskql6jwionWLlGM1Y6QLaw=="
     },
     "node_modules/@github/text-expander-element": {
       "version": "2.6.1",
diff --git a/package.json b/package.json
index 5add488bb6..a0f9b343b4 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
     "@citation-js/plugin-csl": "0.7.11",
     "@citation-js/plugin-software-formats": "0.6.1",
     "@github/markdown-toolbar-element": "2.2.3",
-    "@github/relative-time-element": "4.4.1",
+    "@github/relative-time-element": "4.4.2",
     "@github/text-expander-element": "2.6.1",
     "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
     "@primer/octicons": "19.9.0",

From 45dbeb5600d1f552c0134721fe49e8fd1099b5a4 Mon Sep 17 00:00:00 2001
From: Rowan Bohde <rowan.bohde@gmail.com>
Date: Wed, 12 Jun 2024 06:34:35 -0500
Subject: [PATCH 127/131] Reduce memory usage for chunked artifact uploads to
 MinIO (#31325)

When using the MinIO storage driver for Actions Artifacts, we found that
the chunked artifact required significantly more memory usage to both
upload and merge than the local storage driver. This seems to be related
to hardcoding a value of `-1` for the size to the MinIO client [which
has a warning about memory usage in the respective
docs](https://pkg.go.dev/github.com/minio/minio-go/v7#Client.PutObject).
Specifying the size in both the upload and merge case reduces memory
usage of the MinIO client.

Co-authored-by: Kyle D <kdumontnu@gmail.com>
---
 routers/api/actions/artifacts_chunks.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go
index bba8ec5f94..3d1a3891d9 100644
--- a/routers/api/actions/artifacts_chunks.go
+++ b/routers/api/actions/artifacts_chunks.go
@@ -39,7 +39,7 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
 		r = io.TeeReader(r, hasher)
 	}
 	// save chunk to storage
-	writtenSize, err := st.Save(storagePath, r, -1)
+	writtenSize, err := st.Save(storagePath, r, contentSize)
 	if err != nil {
 		return -1, fmt.Errorf("save chunk to storage error: %v", err)
 	}
@@ -208,7 +208,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
 
 	// save merged file
 	storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension)
-	written, err := st.Save(storagePath, mergedReader, -1)
+	written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize)
 	if err != nil {
 		return fmt.Errorf("save merged file error: %v", err)
 	}

From 21ba5ca03be47a9a7051d13fcfa258bb03dd93df Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 12 Jun 2024 16:58:03 +0200
Subject: [PATCH 128/131] Fix navbar `+` menu flashing on page load (#31281)

Fixes
https://github.com/go-gitea/gitea/pull/31273#issuecomment-2153771331.
Same method as used in https://github.com/go-gitea/gitea/pull/30215. All
left-opening dropdowns need to use it method.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 templates/base/head_navbar.tmpl              |  6 ++---
 templates/repo/issue/labels/label_list.tmpl  | 24 +++++++++-----------
 web_src/css/modules/header.css               |  6 -----
 web_src/css/modules/navbar.css               | 20 ++++++++++++----
 web_src/js/components/DiffCommitSelector.vue |  2 +-
 web_src/js/modules/fomantic/dropdown.js      | 16 +++++++++++++
 6 files changed, 46 insertions(+), 28 deletions(-)

diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 2b52247303..4889924819 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -4,7 +4,7 @@
 {{end}}
 
 <nav id="navbar" aria-label="{{ctx.Locale.Tr "aria.navbar"}}">
-	<div class="navbar-left ui secondary menu">
+	<div class="navbar-left">
 		<!-- the logo -->
 		<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}">
 			<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
@@ -61,7 +61,7 @@
 	</div>
 
 	<!-- the full dropdown menus -->
-	<div class="navbar-right ui secondary menu">
+	<div class="navbar-right">
 		{{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">
@@ -104,7 +104,7 @@
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 					<span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span>
 				</span>
-				<div class="menu left">
+				<div class="menu">
 					<a class="item" href="{{AppSubUrl}}/repo/create">
 						{{svg "octicon-plus"}} {{ctx.Locale.Tr "new_repo"}}
 					</a>
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl
index 8d7fc2c3db..413d6405b2 100644
--- a/templates/repo/issue/labels/label_list.tmpl
+++ b/templates/repo/issue/labels/label_list.tmpl
@@ -1,19 +1,17 @@
 <h4 class="ui top attached header">
 	{{ctx.Locale.Tr "repo.issues.label_count" .NumLabels}}
 	<div class="ui right">
-		<div class="ui secondary menu">
-			<!-- Sort -->
-			<div class="item ui jump dropdown tw-py-2">
-				<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 "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>
+		<!-- Sort -->
+		<div class="item ui jump dropdown tw-py-2">
+			<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 "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> <!-- filter menu -->
diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css
index 9cec5fcbe6..20f98bfbac 100644
--- a/web_src/css/modules/header.css
+++ b/web_src/css/modules/header.css
@@ -134,12 +134,6 @@ 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 {
diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index 848f9331d0..556da2df3b 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -19,12 +19,26 @@
   margin: 0;
   display: flex;
   align-items: center;
+  gap: 5px;
 }
 
 #navbar-logo {
   margin: 0;
 }
 
+.navbar-left > .item,
+.navbar-right > .item {
+  color: var(--color-nav-text);
+  position: relative;
+  text-decoration: none;
+  line-height: var(--line-height-default);
+  flex: 0 0 auto;
+  font-weight: var(--font-weight-normal);
+  align-items: center;
+  padding: .78571429em .92857143em;
+  border-radius: .28571429rem;
+}
+
 #navbar .item {
   min-height: 36px;
   min-width: 36px;
@@ -33,10 +47,6 @@
   display: flex;
 }
 
-#navbar > .menu > .item {
-  color: var(--color-nav-text);
-}
-
 #navbar .dropdown .item {
   justify-content: stretch;
 }
@@ -70,7 +80,7 @@
   }
   #navbar .navbar-mobile-right {
     display: flex;
-    margin-left: auto !important;
+    margin: 0 0 0 auto !important;
     width: auto !important;
   }
   #navbar .navbar-mobile-right > .item {
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
index c28be67e38..6a4a84f615 100644
--- a/web_src/js/components/DiffCommitSelector.vue
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -202,7 +202,7 @@ export default {
     >
       <svg-icon name="octicon-git-commit"/>
     </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="left menu" 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" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">
         <div class="gt-ellipsis">
diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js
index 82e710860d..bbffb59152 100644
--- a/web_src/js/modules/fomantic/dropdown.js
+++ b/web_src/js/modules/fomantic/dropdown.js
@@ -94,6 +94,22 @@ function delegateOne($dropdown) {
     updateSelectionLabel($label[0]);
     return $label;
   });
+
+  const oldSet = dropdownCall('internal', 'set');
+  const oldSetDirection = oldSet.direction;
+  oldSet.direction = function($menu) {
+    oldSetDirection.call(this, $menu);
+    const classNames = dropdownCall('setting', 'className');
+    $menu = $menu || $dropdown.find('> .menu');
+    const elMenu = $menu[0];
+    // detect whether the menu is outside the viewport, and adjust the position
+    // there is a bug in fomantic's builtin `direction` function, in some cases (when the menu width is only a little larger) it wrongly opens the menu at right and triggers the scrollbar.
+    elMenu.classList.add(classNames.loading);
+    if (elMenu.getBoundingClientRect().right > document.documentElement.clientWidth) {
+      elMenu.classList.add(classNames.leftward);
+    }
+    elMenu.classList.remove(classNames.loading);
+  };
 }
 
 // for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes

From 90bcdf9829ebc4ce1636f887cd2b032046078d9c Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 12 Jun 2024 17:23:42 +0200
Subject: [PATCH 129/131] Fix line number widths (#31341)

Fixes regression
https://github.com/go-gitea/gitea/pull/31307#issuecomment-2162554913

Table CSS is weird. A `auto` value does not work and causes the
regression while any pixel value causes another regression in diff where
the code lines do not stretch. Partially revert that PR and clean up
some related too-deep CSS selectors.

<img width="109" alt="Screenshot 2024-06-12 at 15 07 22"
src="https://github.com/go-gitea/gitea/assets/115237/756c5dea-44b8-49f9-8a08-acef68075f62">
<img width="119" alt="Screenshot 2024-06-12 at 15 07 43"
src="https://github.com/go-gitea/gitea/assets/115237/28ae1adc-118e-4016-8d09-033b9f1c9a6f">
<img width="151" alt="Screenshot 2024-06-12 at 15 07 07"
src="https://github.com/go-gitea/gitea/assets/115237/08db7ed9-de4e-405e-874d-c7ebe3082557">
<img width="141" alt="Screenshot 2024-06-12 at 15 07 14"
src="https://github.com/go-gitea/gitea/assets/115237/c4a5492b-1bf1-4773-bc8d-64eb36d823f9">
---
 web_src/css/base.css | 9 +++++++++
 web_src/css/repo.css | 8 --------
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 3bdcde99f6..eef4eb6eff 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1001,6 +1001,13 @@ overflow-menu .ui.label {
   padding: 0 8px;
   text-align: right !important;
   color: var(--color-text-light-2);
+  width: 1%; /* this apparently needs to be a percentage so that code column stretches in diffs */
+  min-width: 72px;
+  white-space: nowrap;
+}
+
+.code-diff .lines-num {
+  min-width: 50px;
 }
 
 .lines-num span.bottom-line::after {
@@ -1020,6 +1027,7 @@ overflow-menu .ui.label {
 
 .lines-type-marker {
   vertical-align: top;
+  white-space: nowrap;
 }
 
 .lines-num,
@@ -1052,6 +1060,7 @@ overflow-menu .ui.label {
 
 .lines-escape {
   width: 0;
+  white-space: nowrap;
 }
 
 .lines-code {
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index e44bc9811b..0e3d06650e 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1555,8 +1555,6 @@ td .commit-summary {
 
 .repository .diff-file-box .file-body.file-code .lines-num {
   text-align: right;
-  width: 1%;
-  min-width: 50px;
 }
 
 .repository .diff-file-box .file-body.file-code .lines-num span.fold {
@@ -1582,12 +1580,6 @@ td .commit-summary {
   table-layout: fixed;
 }
 
-.repository .diff-file-box .code-diff tbody tr td.lines-num,
-.repository .diff-file-box .code-diff tbody tr td.lines-escape,
-.repository .diff-file-box .code-diff tbody tr td.lines-type-marker {
-  white-space: nowrap;
-}
-
 .repository .diff-file-box .code-diff tbody tr td.center {
   text-align: center;
 }

From 7115dce773e3021b3538ae360c4e7344d5bbf45b Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 13 Jun 2024 06:35:46 +0800
Subject: [PATCH 130/131] Fix hash render end with colon (#31319)

Fix a hash render problem like `<hash>: xxxxx` which is usually used in
release notes.
---
 modules/markup/html.go               | 2 +-
 modules/markup/html_internal_test.go | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/modules/markup/html.go b/modules/markup/html.go
index 8dbc958299..565bc175b7 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -49,7 +49,7 @@ var (
 	// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
 	// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length
 	// so that abbreviated hash links can be used as well. This matches git and GitHub usability.
-	hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,](\s|$))`)
+	hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
 
 	// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
 	shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 9aa9c22d70..74089cffdd 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -380,6 +380,7 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
 		"(abcdefabcdefabcdefabcdefabcdefabcdefabcd)",
 		"[abcdefabcdefabcdefabcdefabcdefabcdefabcd]",
 		"abcdefabcdefabcdefabcdefabcdefabcdefabcd.",
+		"abcdefabcdefabcdefabcdefabcdefabcdefabcd:",
 	}
 	falseTestCases := []string{
 		"test",

From 47ca61d8ba41f363745f6d0f93cb8efafa92564b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 13 Jun 2024 09:06:46 +0800
Subject: [PATCH 131/131] Improve detecting empty files (#31332)

Co-authored-by: silverwind <me@silverwind.io>
---
 options/locale/locale_en-US.ini       |  1 +
 routers/web/repo/blame.go             |  2 --
 routers/web/repo/setting/lfs.go       |  1 +
 routers/web/repo/view.go              |  7 +++----
 templates/repo/blame.tmpl             |  2 ++
 templates/repo/file_info.tmpl         |  4 ++--
 templates/repo/settings/lfs_file.tmpl |  6 ++----
 templates/repo/view_file.tmpl         |  2 ++
 templates/shared/fileisempty.tmpl     |  3 +++
 templates/shared/filetoolarge.tmpl    |  2 +-
 web_src/css/repo.css                  | 12 ++++++++++++
 11 files changed, 29 insertions(+), 13 deletions(-)
 create mode 100644 templates/shared/fileisempty.tmpl

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 539715b3f9..fbada5472c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1238,6 +1238,7 @@ file_view_rendered = View Rendered
 file_view_raw = View Raw
 file_permalink = Permalink
 file_too_large = The file is too large to be shown.
+file_is_empty = The file is empty.
 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`
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 1887e4d95d..3e76ea6df4 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -99,8 +99,6 @@ func RefBlame(ctx *context.Context) {
 	}
 
 	ctx.Data["NumLines"], err = blob.GetBlobLineCount()
-	ctx.Data["NumLinesSet"] = true
-
 	if err != nil {
 		ctx.NotFound("GetBlobLineCount", err)
 		return
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 6dddade066..2891556d6f 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -303,6 +303,7 @@ func LFSFileGet(ctx *context.Context) {
 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 
 		// Building code view blocks with line number on server side.
+		// FIXME: the logic is not right here: it first calls EscapeControlReader then calls HTMLEscapeString: double-escaping
 		escapedContent := &bytes.Buffer{}
 		ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale)
 
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 386ef7be5c..0aa3fe1efd 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -286,6 +286,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
 
 	ctx.Data["FileIsText"] = fInfo.isTextFile
 	ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
+	ctx.Data["FileSize"] = fInfo.fileSize
 	ctx.Data["IsLFSFile"] = fInfo.isLFSFile
 
 	if fInfo.isLFSFile {
@@ -301,7 +302,6 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
 		// Pretend that this is a normal text file to display 'This file is too large to be shown'
 		ctx.Data["IsFileTooLarge"] = true
 		ctx.Data["IsTextFile"] = true
-		ctx.Data["FileSize"] = fInfo.fileSize
 		return
 	}
 
@@ -552,7 +552,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			} else {
 				ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
 			}
-			ctx.Data["NumLinesSet"] = true
 
 			language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
 			if err != nil {
@@ -606,8 +605,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?
+		// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
+		// It is used by "external renders", markupRender will execute external programs to get rendered content.
 		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 4ad3ed85c9..3e7cd92066 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -32,6 +32,8 @@
 		<div class="file-view code-view unicode-escaped">
 			{{if .IsFileTooLarge}}
 				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else if not .FileSize}}
+				{{template "shared/fileisempty"}}
 			{{else}}
 			<table>
 				<tbody>
diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl
index 823cf1b7d8..b63af68973 100644
--- a/templates/repo/file_info.tmpl
+++ b/templates/repo/file_info.tmpl
@@ -4,12 +4,12 @@
 			{{ctx.Locale.Tr "repo.symbolic_link"}}
 		</div>
 	{{end}}
-	{{if .NumLinesSet}}{{/* Explicit attribute needed to show 0 line changes */}}
+	{{if ne .NumLines nil}}
 		<div class="file-info-entry">
 			{{.NumLines}} {{ctx.Locale.TrN .NumLines "repo.line" "repo.lines"}}
 		</div>
 	{{end}}
-	{{if .FileSize}}
+	{{if ne .FileSize nil}}
 		<div class="file-info-entry">
 			{{FileSize .FileSize}}{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
 		</div>
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
index a015cc8bd1..f6fac05b69 100644
--- a/templates/repo/settings/lfs_file.tmpl
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -16,10 +16,8 @@
 				<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
 					{{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>
+					{{else if not .FileSize}}
+						{{template "shared/fileisempty"}}
 					{{else if not .IsTextFile}}
 						<div class="view-raw">
 							{{if .IsImageFile}}
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 0a34b6c325..0ec400cfe9 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -91,6 +91,8 @@
 		<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
 			{{if .IsFileTooLarge}}
 				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else if not .FileSize}}
+				{{template "shared/fileisempty"}}
 			{{else if .IsMarkup}}
 				{{if .FileContent}}{{.FileContent}}{{end}}
 			{{else if .IsPlainText}}
diff --git a/templates/shared/fileisempty.tmpl b/templates/shared/fileisempty.tmpl
new file mode 100644
index 0000000000..a92bcbcdbc
--- /dev/null
+++ b/templates/shared/fileisempty.tmpl
@@ -0,0 +1,3 @@
+<div class="file-not-rendered-prompt">
+	{{ctx.Locale.Tr "repo.file_is_empty"}}
+</div>
diff --git a/templates/shared/filetoolarge.tmpl b/templates/shared/filetoolarge.tmpl
index 8842fb1b91..cb23864ec8 100644
--- a/templates/shared/filetoolarge.tmpl
+++ b/templates/shared/filetoolarge.tmpl
@@ -1,4 +1,4 @@
-<div class="tw-p-4">
+<div class="file-not-rendered-prompt">
 	{{ctx.Locale.Tr "repo.file_too_large"}}
 	{{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}}
 </div>
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 0e3d06650e..357a4ee195 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1706,6 +1706,18 @@ td .commit-summary {
 .file-view.markup {
   padding: 1em 2em;
 }
+
+.file-view.markup:has(.file-not-rendered-prompt) {
+  padding: 0; /* let the file-not-rendered-prompt layout itself */
+}
+
+.file-not-rendered-prompt {
+  padding: 1rem;
+  text-align: center;
+  font-size: 1rem !important; /* use consistent styles for various containers (code, markup, etc) */
+  line-height: var(--line-height-default) !important; /* same as above */
+}
+
 .repository .activity-header {
   display: flex;
   justify-content: space-between;