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.  The latest commit is 2 weeks ago.  The notification comes from another repo with same branch name:  After: In forked repo:  New PR Link will redirect to the original repo:  In the original repo:  New PR Link:  In the same repo:  New PR Link:  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>  </details> <details><summary>After (unexpanded)</summary>  </details> <details><summary>After (expanded)</summary>  </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 -[](https://discord.gg/Gitea) -[](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: 文档 -[](http://drone.gitea.io/go-gitea/docs) -[](https://discord.gg/Gitea) -[](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  --- 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:  --- 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><script>alert(document.domain)</script></scrİpt>", "<script>alert(document.domain)</script>", // 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><script>alert(document.domain)</script></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&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&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 = ``; } 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)  --- 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 }} </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:  --- 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("&") - case `'`: - _, err = tmpBlock.WriteString("'") // "'" is shorter than "'" and apos was not in HTML until HTML5. - case `<`: - _, err = tmpBlock.WriteString("<") - case `>`: - _, err = tmpBlock.WriteString(">") - case `"`: - _, err = tmpBlock.WriteString(""") // """ is shorter than """. - 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,<a>\n2,<b></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:  ***The problem was that the icon and text were not on a horizontal line, and the horizontal was not centered;*** after:  --------- 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:  after:  --- 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> {{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> {{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> {{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> {{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;