From 5b56d13e0d0ebb2b70c54567104faf4a208d0ab1 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 14 Jun 2024 00:27:38 +0000 Subject: [PATCH 01/17] [skip ci] Updated translations via Crowdin --- options/locale/locale_cs-CZ.ini | 48 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index acc0d0bc27..4cbac49ddc 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -164,6 +164,8 @@ search=Hledat... type_tooltip=Druh vyhledávání fuzzy=Fuzzy fuzzy_tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu +exact=Přesně +exact_tooltip=Zahrnout pouze výsledky, které přesně odpovídají hledanému výrazu repo_kind=Hledat repozitáře... user_kind=Hledat uživatele... org_kind=Hledat organizace... @@ -177,6 +179,8 @@ branch_kind=Hledat větve... commit_kind=Hledat commity... runner_kind=Hledat runnery... no_results=Nebyly nalezeny žádné odpovídající výsledky. +issue_kind=Hledat úkoly... +pull_kind=Hledat pull request... keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu. [aria] @@ -432,6 +436,7 @@ oauth_signin_submit=Propojit účet oauth.signin.error=Došlo k chybě při zpracování žádosti o autorizaci. Pokud tato chyba přetrvává, obraťte se na správce webu. oauth.signin.error.access_denied=Žádost o autorizaci byla zamítnuta. oauth.signin.error.temporarily_unavailable=Autorizace se nezdařila, protože ověřovací server je dočasně nedostupný. Opakujte akci později. +oauth_callback_unable_auto_reg=Automatická registrace je povolena, ale OAuth2 poskytovatel %[1]s vrátil chybějící pole: %[2]s, nelze vytvořit účet automaticky, vytvořte účet nebo se připojte k účtu, nebo kontaktujte správce webu. openid_connect_submit=Připojit openid_connect_title=Připojení k existujícímu účtu openid_connect_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde. @@ -712,8 +717,9 @@ cancel=Zrušit language=Jazyk ui=Motiv vzhledu hidden_comment_types=Skryté typy komentářů +hidden_comment_types_description=Zde zaškrtnuté typy komentářů nebudou zobrazeny na stránkách úkolů. Zaškrtnutím položky „Štítek“ například odstraní všechny komentáře „{uživatel} přidal/odstranil {štítek}“. hidden_comment_types.ref_tooltip=Komentáře, na které se odkazovalo z jiného úkolu/commitu/… -hidden_comment_types.issue_ref_tooltip=Komentáře, kde uživatel změní větev/značku spojenou s problémem +hidden_comment_types.issue_ref_tooltip=Komentáře, kde uživatel změní větev/značku spojenou s úkolem comment_type_group_reference=Reference comment_type_group_label=Štítek comment_type_group_milestone=Milník @@ -758,6 +764,8 @@ manage_themes=Vyberte výchozí motiv vzhledu manage_openid=Správa OpenID adres email_desc=Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla, a pokud není skrytá, pro operace Gitu. theme_desc=Toto bude váš výchozí motiv vzhledu napříč stránkou. +theme_colorblindness_help=Podpora šablony pro barvoslepost +theme_colorblindness_prompt=Gitea právě získala některé motivy se základní podporou barvosleposti, které mají pouze několik barev. Práce stále probíhá. Další vylepšení by bylo možné provést definováním více barev v CSS souborů. primary=Hlavní activated=Aktivován requires_activation=Vyžaduje aktivaci @@ -882,6 +890,7 @@ repo_and_org_access=Repozitář a přístup organizace permissions_public_only=Pouze veřejnost permissions_access_all=Vše (veřejné, soukromé a omezené) select_permissions=Vyberte oprávnění +permission_not_set=Není nastaveno permission_no_access=Bez přístupu permission_read=Přečtené permission_write=čtení i zápis @@ -1061,6 +1070,7 @@ watchers=Sledující stargazers=Sledující stars_remove_warning=Tímto odstraníte všechny hvězdičky z tohoto repozitáře. forks=Rozštěpení +stars=Oblíbené reactions_more=a %d dalších unit_disabled=Správce webu zakázal tuto sekci repozitáře. language_other=Jiný @@ -1108,7 +1118,7 @@ template.one_item=Musíte vybrat alespoň jednu položku šablony template.invalid=Musíte vybrat repositář šablony archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo pull requesty. -archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo pull requesty. +archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat úkoly nebo pull requesty. archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly. archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat pull requesty. @@ -1228,6 +1238,8 @@ file_view_rendered=Zobrazit vykreslené file_view_raw=Zobrazit v surovém stavu file_permalink=Trvalý odkaz file_too_large=Soubor je příliš velký pro zobrazení. +code_preview_line_from_to=Řádky %[1]d do%[2]d v %[3]s +code_preview_line_in=Řádek %[1]d v %[2]s invisible_runes_header=`Tento soubor obsahuje neviditelné znaky Unicode` invisible_runes_description=`Tento soubor obsahuje neviditelné Unicode znaky, které jsou pro člověka nerozeznatelné, ale mohou být zpracovány jiným způsobem. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.` ambiguous_runes_header=`Tento soubor obsahuje nejednoznačné znaky Unicode` @@ -1282,6 +1294,7 @@ editor.or=nebo editor.cancel_lower=Zrušit editor.commit_signed_changes=Odevzdat podepsané změny editor.commit_changes=Odevzdat změny +editor.add_tmpl=Přidán „{nazev_souboru}“ editor.add=Přidat %s editor.update=Aktualizovat %s editor.delete=Odstranit %s @@ -1364,6 +1377,7 @@ commitstatus.success=Úspěch ext_issues=Přístup k externím úkolům ext_issues.desc=Odkaz na externí systém úkolů. +projects.desc=Spravujte úkoly a pull requesty v projektech. projects.description=Popis (volitelné) projects.description_placeholder=Popis projects.create=Vytvořit projekt @@ -1391,6 +1405,7 @@ projects.column.new=Nový sloupec projects.column.set_default=Nastavit jako výchozí projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekategorizované úkoly a požadavky na natažení projects.column.delete=Smazat sloupec +projects.column.deletion_desc=Smazání sloupce projektu přesune všechny související úkoly do výchozího sloupce. Pokračovat? projects.column.color=Barva projects.open=Otevřít projects.close=Zavřít @@ -1426,6 +1441,7 @@ issues.new.clear_assignees=Smazat zpracovatele issues.new.no_assignees=Bez zpracovatelů issues.new.no_reviewers=Žádní posuzovatelé issues.new.blocked_user=Nemůžete vytvořit úkol, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. +issues.edit.already_changed=Nelze uložit změny v úkolu. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste ji znovu problém upravit, abyste se vyhnuli přepsání jejich změn issues.edit.blocked_user=Nemůžete upravovat obsah, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. issues.choose.get_started=Začínáme issues.choose.open_external_link=Otevřít @@ -1433,7 +1449,7 @@ issues.choose.blank=Výchozí issues.choose.blank_about=Vytvořit úkol z výchozí šablony. issues.choose.ignore_invalid_templates=Neplatné šablony byly ignorovány issues.choose.invalid_templates=%v nalezených neplatných šablon -issues.choose.invalid_config=Nastavení problému obsahuje chyby: +issues.choose.invalid_config=Nastavení úkolu obsahuje chyby: issues.no_ref=Není určena žádná větev/značka issues.create=Vytvořit úkol issues.new_label=Nový štítek @@ -1534,10 +1550,12 @@ issues.context.reference_issue=Odkázat v novém úkolu issues.context.edit=Upravit issues.context.delete=Smazat issues.no_content=K dispozici není žádný popis. -issues.close=Zavřít problém +issues.close=Zavřít úkol 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=Znovu otevřít s komentářem 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 %[2]s` @@ -1598,7 +1616,7 @@ issues.attachment.open_tab=`Klikněte pro zobrazení „%s“ v nové záložce` issues.attachment.download=`Klikněte pro stažení „%s“` issues.subscribe=Odebírat issues.unsubscribe=Zrušit odběr -issues.unpin_issue=Odepnout problém +issues.unpin_issue=Odepnout úkol issues.max_pinned=Nemůžete připnout další úkoly issues.pin_comment=připnuto %s issues.unpin_comment=odepnul/a tento %s @@ -1657,7 +1675,7 @@ issues.due_date_form=rrrr-mm-dd issues.due_date_form_add=Přidat termín dokončení issues.due_date_form_edit=Upravit issues.due_date_form_remove=Odstranit -issues.due_date_not_writer=Potřebujete přístup k zápisu do tohoto repozitáře, abyste mohli aktualizovat datum dokončení problému. +issues.due_date_not_writer=Potřebujete přístup k zápisu do tohoto repozitáře, abyste mohli aktualizovat datum dokončení úkolu. issues.due_date_not_set=Žádný termín dokončení. issues.due_date_added=přidal/a termín dokončení %s %s issues.due_date_modified=upravil/a termín termínu z %[2]s na %[1]s %[3]s @@ -1739,6 +1757,7 @@ compare.compare_head=porovnat pulls.desc=Povolit pull requesty a posuzování kódu. pulls.new=Nový pull request pulls.new.blocked_user=Nemůžete vytvořit pull request, protože jste zablokování vlastníkem repozitáře. +pulls.edit.already_changed=Nelze uložit změny v pull requestu. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste znovu komentář upravit, abyste se vyhnuli přepsání jejich změn pulls.view=Zobrazit pull request pulls.compare_changes=Nový pull request pulls.allow_edits_from_maintainers=Povolit úpravy od správců @@ -1858,6 +1877,7 @@ pulls.close=Zavřít pull request pulls.closed_at=`uzavřel/a tento pull request %[2]s` pulls.reopened_at=`znovuotevřel/a tento pull request %[2]s` pulls.cmd_instruction_hint=`Zobrazit instrukce příkazové řádky.` +pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny. pulls.cmd_instruction_merge_title=Sloučit pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea. @@ -1883,6 +1903,7 @@ pulls.recently_pushed_new_branches=Nahráli jste větev %[1]s % pull.deleted_branch=(odstraněno):%s +comments.edit.already_changed=Nelze uložit změny v komentáři. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste znovu komentář upravit, abyste se vyhnuli přepsání jejich změn milestones.new=Nový milník milestones.closed=Zavřen dne %s @@ -1959,6 +1980,7 @@ wiki.page_name_desc=Zadejte název této Wiki stránky. Některé speciální n wiki.original_git_entry_tooltip=Zobrazit originální Git soubor namísto použití přátelského odkazu. activity=Aktivita +activity.navbar.pulse=Pulz activity.navbar.code_frequency=Frekvence kódu activity.navbar.contributors=Přispěvatelé activity.navbar.recent_commits=Nedávné commity @@ -2052,6 +2074,7 @@ settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=Právě t settings.mirror_settings.docs.disabled_push_mirror.info=Push zrcadla byla zakázána administrátorem vašeho webu. settings.mirror_settings.docs.no_new_mirrors=Váš repozitář zrcadlí změny do nebo z jiného repozitáře. Mějte prosím na paměti, že v tuto chvíli nemůžete vytvořit žádná nová zrcadla. settings.mirror_settings.docs.can_still_use=I když nemůžete upravit stávající zrcadla nebo vytvořit nová, stále můžete použít své stávající zrcadlo. +settings.mirror_settings.docs.pull_mirror_instructions=Chcete-li nastavit zrcadlo pro natažení, konzultujte prosím: settings.mirror_settings.docs.more_information_if_disabled=Více informací o zrcadlech pro nahrání a natažení naleznete zde: settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře? settings.mirror_settings.docs.doc_link_pull_section=sekci "stahovat ze vzdáleného úložiště" v dokumentaci. @@ -2079,6 +2102,7 @@ settings.advanced_settings=Pokročilá nastavení settings.wiki_desc=Povolit Wiki repozitáře settings.use_internal_wiki=Používat vestavěnou Wiki settings.default_wiki_branch_name=Výchozí název větve Wiki +settings.default_wiki_everyone_access=Výchozí přístupová práva pro přihlášené uživatele: settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila. settings.use_external_wiki=Používat externí Wiki settings.external_wiki_url=URL externí Wiki @@ -2760,6 +2784,7 @@ teams.invite.by=Pozvání od %s teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže. [admin] +maintenance=Údržba dashboard=Přehled self_check=Samokontrola identity_access=Identita a přístup @@ -2782,6 +2807,7 @@ settings=Nastavení správce dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na blogu pro více informací. dashboard.statistic=Souhrn +dashboard.maintenance_operations=Operace údržby dashboard.system_status=Status systému dashboard.operation_name=Název operace dashboard.operation_switch=Přepnout @@ -3067,12 +3093,14 @@ auths.tips=Tipy auths.tips.oauth2.general=Ověřování OAuth2 auths.tips.oauth2.general.tip=Při registraci nové OAuth2 autentizace by URL callbacku/přesměrování měla být: auths.tip.oauth2_provider=Poskytovatel OAuth2 +auths.tip.bitbucket=Vytvořte nového OAuth konzumenta na https://bitbucket.org/account/user/{vase-uzivatelske-jmeno}/oauth-consumers/new a přidejte oprávnění „Account“ - „Read“ auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci pomocí následujícího menu „Nastavení -> Zabezpečení -> OAuth 2.0 klient“ auths.tip.dropbox=Vytvořte novou aplikaci na https://www.dropbox.com/developers/apps auths.tip.facebook=Registrujte novou aplikaci na https://developers.facebook.com/apps a přidejte produkt „Facebook Login“ auths.tip.github=Registrujte novou OAuth aplikaci na https://github.com/settings/applications/new auths.tip.gitlab_new=Zaregistrujte novou aplikaci na https://gitlab.com/-/profile/applications auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzole na https://console.developers.google.com/ +auths.tip.openid_connect=Použijte OpenID Connect URL pro objevování spojení „https://{server}/.well-known/openid-configuration“ k nastavení koncových bodů auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená auths.tip.discord=Registrujte novou aplikaci na https://discordapp.com/developers/applications/me auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://docs.gitea.com/development/oauth2-provider @@ -3285,11 +3313,13 @@ notices.op=Akce notices.delete_success=Systémové upozornění bylo smazáno. self_check.no_problem_found=Zatím nebyl nalezen žádný problém. +self_check.startup_warnings=Upozornění při spuštění: self_check.database_collation_mismatch=Očekávejte, že databáze použije collation: %s self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Gitea může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání. self_check.database_inconsistent_collation_columns=Databáze používá collation %s, ale tyto sloupce používají chybné collation. To může způsobit neočekávané problémy. self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít příkaz "gitea doctor convert", který opraví problémy s collation, nebo můžete také problém vyřešit příkazem "ALTER ... COLLATE ..." SQL ručně. self_check.database_fix_mssql=Uživatelé MSSQL mohou problém vyřešit pouze pomocí příkazu "ALTER ... COLLATE ..." SQL ručně. +self_check.location_origin_mismatch=Aktuální URL (%[1]s) se neshoduje s URL viditelnou pro Gitea (%[2]s). Pokud používáte reverzní proxy, ujistěte se, že hlavičky „Host“ a „X-Forwarded-Proto“ jsou nastaveny správně. [action] create_repo=vytvořil/a repozitář %s @@ -3301,7 +3331,7 @@ reopen_issue=`znovuotevřel/a úkol %[3]s#%[2]s` create_pull_request=`vytvořil/a pull request %[3]s#%[2]s` close_pull_request=`uzavřel/a pull request %[3]s#%[2]s` reopen_pull_request=`znovuotevřel/a pull request %[3]s#%[2]s` -comment_issue=`okomentoval/a problém %[3]s#%[2]s` +comment_issue=`okomentoval/a úkol %[3]s#%[2]s` comment_pull=`okomentoval/a pull request %[3]s#%[2]s` merge_pull_request=`sloučil/a pull request %[3]s#%[2]s` auto_merge_pull_request=`automaticky sloučen pull request %[3]s#%[2]s` @@ -3317,6 +3347,7 @@ mirror_sync_create=synchronizoval/a novou referenci %[3]s do mirror_sync_delete=synchronizoval/a a smazal/a referenci %[2]s v %[3]s ze zrcadla approve_pull_request=`schválil/a %[3]s#%[2]s` reject_pull_request=`navrhl/a změny pro %[3]s#%[2]s` +publish_release=`vydal/a "%[4]s" v %[3]s` review_dismissed=`zamítl/a posouzení z %[4]s pro %[3]s#%[2]s` review_dismissed_reason=Důvod: create_branch=vytvořil/a větev %[3]s v %[4]s @@ -3383,6 +3414,7 @@ error.unit_not_allowed=Nejste oprávněni přistupovat k této části repozitá title=Balíčky desc=Správa balíčků repozitáře. empty=Zatím nejsou žádné balíčky. +no_metadata=Žádná metadata. empty.documentation=Další informace o registru balíčků naleznete v dokumentaci. empty.repo=Nahráli jste balíček, ale nezobrazil se zde? Přejděte na nastavení balíčku a propojte jej s tímto repozitářem. registry.documentation=Další informace o registru %s naleznete v dokumentaci. @@ -3464,6 +3496,7 @@ npm.install=Pro instalaci balíčku pomocí npm spusťte následující příkaz npm.install2=nebo ho přidejte do souboru package.json: npm.dependencies=Závislosti npm.dependencies.development=Vývojové závislosti +npm.dependencies.bundle=Vnitřní závislosti npm.dependencies.peer=Vzájemné závislosti npm.dependencies.optional=Volitelné závislosti npm.details.tag=Značka @@ -3601,6 +3634,7 @@ runs.pushed_by=náhrán runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s runs.no_job_without_needs=Pracovní postup musí obsahovat alespoň jednu úlohu bez závislostí. +runs.no_job=Pracovní postup musí obsahovat alespoň jednu úlohu runs.actor=Aktér runs.status=Status runs.actors_no_select=Všichni aktéři From 1761459ebc7eb6d432eced093b4583425a5c5d4b Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Fri, 14 Jun 2024 04:26:33 +0300 Subject: [PATCH 02/17] Refactor to use UnsafeStringToBytes (#31358) The PR replaces all `goldmark/util.BytesToReadOnlyString` with `util.UnsafeBytesToString`, `goldmark/util.StringToReadOnlyBytes` with `util.UnsafeStringToBytes`. This removes one `TODO`. Co-authored-by: wxiaoguang --- modules/markup/markdown/prefixed_id.go | 6 +++--- modules/markup/markdown/transform_heading.go | 6 +++--- modules/references/references.go | 5 ++--- modules/system/db.go | 7 +++---- modules/util/sanitize.go | 6 ++---- modules/util/string.go | 2 +- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/modules/markup/markdown/prefixed_id.go b/modules/markup/markdown/prefixed_id.go index 9c60949202..63d7fadc0a 100644 --- a/modules/markup/markdown/prefixed_id.go +++ b/modules/markup/markdown/prefixed_id.go @@ -9,9 +9,9 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/markup/common" + "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/util" ) type prefixedIDs struct { @@ -36,7 +36,7 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { if !bytes.HasPrefix(result, []byte("user-content-")) { result = append([]byte("user-content-"), result...) } - if p.values.Add(util.BytesToReadOnlyString(result)) { + if p.values.Add(util.UnsafeBytesToString(result)) { return result } for i := 1; ; i++ { @@ -49,7 +49,7 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { // Put puts a given element id to the used ids table. func (p *prefixedIDs) Put(value []byte) { - p.values.Add(util.BytesToReadOnlyString(value)) + p.values.Add(util.UnsafeBytesToString(value)) } func newPrefixedIDs() *prefixedIDs { diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go index 6f38abfad9..6d48f34d93 100644 --- a/modules/markup/markdown/transform_heading.go +++ b/modules/markup/markdown/transform_heading.go @@ -7,10 +7,10 @@ import ( "fmt" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" ) func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) { @@ -21,11 +21,11 @@ func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Headin } txt := v.Text(reader.Source()) header := markup.Header{ - Text: util.BytesToReadOnlyString(txt), + Text: util.UnsafeBytesToString(txt), Level: v.Level, } if id, found := v.AttributeString("id"); found { - header.ID = util.BytesToReadOnlyString(id.([]byte)) + header.ID = util.UnsafeBytesToString(id.([]byte)) } *tocList = append(*tocList, header) g.applyElementDir(v) diff --git a/modules/references/references.go b/modules/references/references.go index 1b656ed4cb..2889430bcf 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -14,8 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/mdstripper" "code.gitea.io/gitea/modules/setting" - - "github.com/yuin/goldmark/util" + "code.gitea.io/gitea/modules/util" ) var ( @@ -341,7 +340,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool return false, nil } } - r := getCrossReference(util.StringToReadOnlyBytes(content), match[2], match[3], false, prOnly) + r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly) if r == nil { return false, nil } diff --git a/modules/system/db.go b/modules/system/db.go index 05e9de0ae8..17178283d9 100644 --- a/modules/system/db.go +++ b/modules/system/db.go @@ -8,8 +8,7 @@ import ( "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/json" - - "github.com/yuin/goldmark/util" + "code.gitea.io/gitea/modules/util" ) // DBStore can be used to store app state items in local filesystem @@ -24,7 +23,7 @@ func (f *DBStore) Get(ctx context.Context, item StateItem) error { if content == "" { return nil } - return json.Unmarshal(util.StringToReadOnlyBytes(content), item) + return json.Unmarshal(util.UnsafeStringToBytes(content), item) } // Set saves the state item @@ -33,5 +32,5 @@ func (f *DBStore) Set(ctx context.Context, item StateItem) error { if err != nil { return err } - return system.SaveAppStateContent(ctx, item.Name(), util.BytesToReadOnlyString(b)) + return system.SaveAppStateContent(ctx, item.Name(), util.UnsafeBytesToString(b)) } diff --git a/modules/util/sanitize.go b/modules/util/sanitize.go index f1ea2574f1..0dd8b342a2 100644 --- a/modules/util/sanitize.go +++ b/modules/util/sanitize.go @@ -6,8 +6,6 @@ package util import ( "bytes" "unicode" - - "github.com/yuin/goldmark/util" ) type sanitizedError struct { @@ -33,7 +31,7 @@ var schemeSep = []byte("://") // SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://") for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com" func SanitizeCredentialURLs(s string) string { - bs := util.StringToReadOnlyBytes(s) + bs := UnsafeStringToBytes(s) schemeSepPos := bytes.Index(bs, schemeSep) if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 { return s // fast return if there is no URL scheme or no userinfo @@ -70,5 +68,5 @@ func SanitizeCredentialURLs(s string) string { schemeSepPos = bytes.Index(bs, schemeSep) } out = append(out, bs...) - return util.BytesToReadOnlyString(out) + return UnsafeBytesToString(out) } diff --git a/modules/util/string.go b/modules/util/string.go index 2cf44d29b1..cf50f591c6 100644 --- a/modules/util/string.go +++ b/modules/util/string.go @@ -87,11 +87,11 @@ func ToSnakeCase(input string) string { } // UnsafeBytesToString uses Go's unsafe package to convert a byte slice to a string. -// TODO: replace all "goldmark/util.BytesToReadOnlyString" with this official approach func UnsafeBytesToString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) } +// UnsafeStringToBytes uses Go's unsafe package to convert a string to a byte slice. func UnsafeStringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } From e4abaff7ffbbc5acd3aa668a9c458fbdf76f9573 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 14 Jun 2024 10:31:07 +0800 Subject: [PATCH 03/17] Fix bug filtering issues which have no project (#31337) Fix #31327 This is a quick patch to fix the bug. Some parameters are using 0, some are using -1. I think it needs a refactor to keep consistent. But that will be another PR. --- modules/indexer/issues/dboptions.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index d9cf9b5e3b..50916024af 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -38,6 +38,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.MilestoneIDs = opts.MilestoneIDs } + if opts.ProjectID > 0 { + searchOpt.ProjectID = optional.Some(opts.ProjectID) + } else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places + searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0) + } + // See the comment of issues_model.SearchOptions for the reason why we need to convert convertID := func(id int64) optional.Option[int64] { if id > 0 { @@ -49,7 +55,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp return nil } - searchOpt.ProjectID = convertID(opts.ProjectID) searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.PosterID = convertID(opts.PosterID) searchOpt.AssigneeID = convertID(opts.AssigneeID) From fa82a8af12ece9eaf78a0cf9d9330adb60c01724 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 14 Jun 2024 11:17:05 +0800 Subject: [PATCH 04/17] Have new announcement about docs contributions (#31364) According to the maintainers' discussion and voting. We decide to move docs to https://gitea.com/gitea/docs . Add some hints on this repository to not make contributors confusing. --- .github/pull_request_template.md | 11 ++++++----- CONTRIBUTING.md | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b752abb794..b7594a1ba7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,10 @@ - + Please check the following: 1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports. 2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md . -3. Describe what your pull request does and which issue you're targeting (if any). -4. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. -5. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. -6. Delete all these tips before posting. +3. For documentations contribution, please go to https://gitea.com/gitea/docs +4. Describe what your pull request does and which issue you're targeting (if any). +5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. +6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. +7. Delete all these tips before posting. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04c06ffd14..60146276db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -358,7 +358,8 @@ $REWRITTEN_PR_SUMMARY ## Documentation -If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in the same PR. +If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs). +**The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.** ## API v1 From 4e7b067a7fdfb3e2c8dfdf87475e3938051fd400 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 14 Jun 2024 06:45:52 +0200 Subject: [PATCH 05/17] Extract and display readme and comments for Composer packages (#30927) Related #30075 CC @thojo0 Example with rendered readme: ![grafik](https://github.com/go-gitea/gitea/assets/1666336/3516fef5-2631-40fd-8841-5d9894ec8904) --- modules/packages/composer/metadata.go | 44 ++++++++++++++- modules/packages/composer/metadata_test.go | 62 +++++++++++++++------- templates/package/content/composer.tmpl | 10 ++-- 3 files changed, 90 insertions(+), 26 deletions(-) diff --git a/modules/packages/composer/metadata.go b/modules/packages/composer/metadata.go index 1d0f025648..2c2e9ebf27 100644 --- a/modules/packages/composer/metadata.go +++ b/modules/packages/composer/metadata.go @@ -6,6 +6,7 @@ package composer import ( "archive/zip" "io" + "path" "regexp" "strings" @@ -36,10 +37,14 @@ type Package struct { Metadata *Metadata } +// https://getcomposer.org/doc/04-schema.md + // Metadata represents the metadata of a Composer package type Metadata struct { Description string `json:"description,omitempty"` + Readme string `json:"readme,omitempty"` Keywords []string `json:"keywords,omitempty"` + Comments Comments `json:"_comments,omitempty"` Homepage string `json:"homepage,omitempty"` License Licenses `json:"license,omitempty"` Authors []Author `json:"authors,omitempty"` @@ -74,6 +79,28 @@ func (l *Licenses) UnmarshalJSON(data []byte) error { return nil } +// Comments represents the comments of a Composer package +type Comments []string + +// UnmarshalJSON reads from a string or array +func (c *Comments) UnmarshalJSON(data []byte) error { + switch data[0] { + case '"': + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *c = Comments{value} + case '[': + values := make([]string, 0, 5) + if err := json.Unmarshal(data, &values); err != nil { + return err + } + *c = Comments(values) + } + return nil +} + // Author represents an author type Author struct { Name string `json:"name,omitempty"` @@ -101,14 +128,14 @@ func ParsePackage(r io.ReaderAt, size int64) (*Package, error) { } defer f.Close() - return ParseComposerFile(f) + return ParseComposerFile(archive, path.Dir(file.Name), f) } } return nil, ErrMissingComposerFile } // ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package -func ParseComposerFile(r io.Reader) (*Package, error) { +func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Package, error) { var cj struct { Name string `json:"name"` Version string `json:"version"` @@ -137,6 +164,19 @@ func ParseComposerFile(r io.Reader) (*Package, error) { cj.Type = "library" } + if cj.Readme == "" { + cj.Readme = "README.md" + } + f, err := archive.Open(path.Join(pathPrefix, cj.Readme)) + if err == nil { + // 10kb limit for readme content + buf, _ := io.ReadAll(io.LimitReader(f, 10*1024)) + cj.Readme = string(buf) + _ = f.Close() + } else { + cj.Readme = "" + } + return &Package{ Name: cj.Name, Version: cj.Version, diff --git a/modules/packages/composer/metadata_test.go b/modules/packages/composer/metadata_test.go index a0e1a77a6e..a5e317daf1 100644 --- a/modules/packages/composer/metadata_test.go +++ b/modules/packages/composer/metadata_test.go @@ -17,6 +17,8 @@ import ( const ( name = "gitea/composer-package" description = "Package Description" + readme = "Package Readme" + comments = "Package Comment" packageType = "composer-plugin" author = "Gitea Authors" email = "no.reply@gitea.io" @@ -41,7 +43,8 @@ const composerContent = `{ }, "require": { "php": ">=7.2 || ^8.0" - } + }, + "_comments": "` + comments + `" }` func TestLicenseUnmarshal(t *testing.T) { @@ -54,18 +57,30 @@ func TestLicenseUnmarshal(t *testing.T) { assert.Equal(t, "MIT", l[0]) } +func TestCommentsUnmarshal(t *testing.T) { + var c Comments + assert.NoError(t, json.NewDecoder(strings.NewReader(`["comment"]`)).Decode(&c)) + assert.Len(t, c, 1) + assert.Equal(t, "comment", c[0]) + assert.NoError(t, json.NewDecoder(strings.NewReader(`"comment"`)).Decode(&c)) + assert.Len(t, c, 1) + assert.Equal(t, "comment", c[0]) +} + func TestParsePackage(t *testing.T) { - createArchive := func(name, content string) []byte { + createArchive := func(files map[string]string) []byte { var buf bytes.Buffer archive := zip.NewWriter(&buf) - w, _ := archive.Create(name) - w.Write([]byte(content)) + for name, content := range files { + w, _ := archive.Create(name) + w.Write([]byte(content)) + } archive.Close() return buf.Bytes() } t.Run("MissingComposerFile", func(t *testing.T) { - data := createArchive("dummy.txt", "") + data := createArchive(map[string]string{"dummy.txt": ""}) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) assert.Nil(t, cp) @@ -73,7 +88,7 @@ func TestParsePackage(t *testing.T) { }) t.Run("MissingComposerFileInRoot", func(t *testing.T) { - data := createArchive("sub/sub/composer.json", "") + data := createArchive(map[string]string{"sub/sub/composer.json": ""}) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) assert.Nil(t, cp) @@ -81,43 +96,52 @@ func TestParsePackage(t *testing.T) { }) t.Run("InvalidComposerFile", func(t *testing.T) { - data := createArchive("composer.json", "") + data := createArchive(map[string]string{"composer.json": ""}) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) assert.Nil(t, cp) assert.Error(t, err) }) - t.Run("Valid", func(t *testing.T) { - data := createArchive("composer.json", composerContent) + t.Run("InvalidPackageName", func(t *testing.T) { + data := createArchive(map[string]string{"composer.json": "{}"}) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) - assert.NoError(t, err) - assert.NotNil(t, cp) - }) -} - -func TestParseComposerFile(t *testing.T) { - t.Run("InvalidPackageName", func(t *testing.T) { - cp, err := ParseComposerFile(strings.NewReader(`{}`)) assert.Nil(t, cp) assert.ErrorIs(t, err, ErrInvalidName) }) t.Run("InvalidPackageVersion", func(t *testing.T) { - cp, err := ParseComposerFile(strings.NewReader(`{"name": "gitea/composer-package", "version": "1.a.3"}`)) + data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`}) + + cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) assert.Nil(t, cp) assert.ErrorIs(t, err, ErrInvalidVersion) }) + t.Run("InvalidReadmePath", func(t *testing.T) { + data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`}) + + cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) + assert.NoError(t, err) + assert.NotNil(t, cp) + + assert.Empty(t, cp.Metadata.Readme) + }) + t.Run("Valid", func(t *testing.T) { - cp, err := ParseComposerFile(strings.NewReader(composerContent)) + data := createArchive(map[string]string{"composer.json": composerContent, "README.md": readme}) + + cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) assert.NoError(t, err) assert.NotNil(t, cp) assert.Equal(t, name, cp.Name) assert.Empty(t, cp.Version) assert.Equal(t, description, cp.Metadata.Description) + assert.Equal(t, readme, cp.Metadata.Readme) + assert.Len(t, cp.Metadata.Comments, 1) + assert.Equal(t, comments, cp.Metadata.Comments[0]) assert.Len(t, cp.Metadata.Authors, 1) assert.Equal(t, author, cp.Metadata.Authors[0].Name) assert.Equal(t, email, cp.Metadata.Authors[0].Email) diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl index c2dc6345c3..8d48e6c95d 100644 --- a/templates/package/content/composer.tmpl +++ b/templates/package/content/composer.tmpl @@ -22,11 +22,11 @@ - {{if .PackageDescriptor.Metadata.Description}} + {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Comments}}

{{ctx.Locale.Tr "packages.about"}}

-
- {{.PackageDescriptor.Metadata.Description}} -
+ {{if .PackageDescriptor.Metadata.Description}}
{{.PackageDescriptor.Metadata.Description}}
{{end}} + {{if .PackageDescriptor.Metadata.Readme}}
{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}
{{end}} + {{if .PackageDescriptor.Metadata.Comments}}
{{StringUtils.Join .PackageDescriptor.Metadata.Comments " "}}
{{end}} {{end}} {{if or .PackageDescriptor.Metadata.Require .PackageDescriptor.Metadata.RequireDev}} @@ -39,7 +39,7 @@ {{end}} - {{if or .PackageDescriptor.Metadata.Keywords}} + {{if .PackageDescriptor.Metadata.Keywords}}

{{ctx.Locale.Tr "packages.keywords"}}

{{range .PackageDescriptor.Metadata.Keywords}} From d4e4226c3cbfa62a6adf15f4466747468eb208c7 Mon Sep 17 00:00:00 2001 From: mzroot Date: Fri, 14 Jun 2024 19:56:10 +0300 Subject: [PATCH 06/17] Add tag protection via rest api #17862 (#31295) Add tag protection manage via rest API. --------- Co-authored-by: Alexander Kogay Co-authored-by: Giteabot --- models/git/protected_tag.go | 13 ++ modules/structs/repo_tag.go | 28 +++ routers/api/v1/api.go | 9 + routers/api/v1/repo/tag.go | 350 ++++++++++++++++++++++++++++++ routers/api/v1/swagger/options.go | 6 + routers/api/v1/swagger/repo.go | 14 ++ services/convert/convert.go | 26 +++ templates/swagger/v1_json.tmpl | 332 ++++++++++++++++++++++++++++ 8 files changed, 778 insertions(+) diff --git a/models/git/protected_tag.go b/models/git/protected_tag.go index 8a05045651..9a6646c742 100644 --- a/models/git/protected_tag.go +++ b/models/git/protected_tag.go @@ -110,6 +110,19 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) { return tag, nil } +// GetProtectedTagByNamePattern gets protected tag by name_pattern +func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) { + tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID} + has, err := db.GetEngine(ctx).Get(tag) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return tag, nil +} + // IsUserAllowedToControlTag checks if a user can control the specific tag. // It returns true if the tag name is not protected or the user is allowed to control it. func IsUserAllowedToControlTag(ctx context.Context, tags []*ProtectedTag, tagName string, userID int64) (bool, error) { diff --git a/modules/structs/repo_tag.go b/modules/structs/repo_tag.go index 4a7d895288..5722513f4f 100644 --- a/modules/structs/repo_tag.go +++ b/modules/structs/repo_tag.go @@ -3,6 +3,8 @@ package structs +import "time" + // Tag represents a repository tag type Tag struct { Name string `json:"name"` @@ -38,3 +40,29 @@ type CreateTagOption struct { Message string `json:"message"` Target string `json:"target"` } + +// TagProtection represents a tag protection +type TagProtection struct { + ID int64 `json:"id"` + NamePattern string `json:"name_pattern"` + WhitelistUsernames []string `json:"whitelist_usernames"` + WhitelistTeams []string `json:"whitelist_teams"` + // swagger:strfmt date-time + Created time.Time `json:"created_at"` + // swagger:strfmt date-time + Updated time.Time `json:"updated_at"` +} + +// CreateTagProtectionOption options for creating a tag protection +type CreateTagProtectionOption struct { + NamePattern string `json:"name_pattern"` + WhitelistUsernames []string `json:"whitelist_usernames"` + WhitelistTeams []string `json:"whitelist_teams"` +} + +// EditTagProtectionOption options for editing a tag protection +type EditTagProtectionOption struct { + NamePattern *string `json:"name_pattern"` + WhitelistUsernames []string `json:"whitelist_usernames"` + WhitelistTeams []string `json:"whitelist_teams"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 74062c44ac..5363489939 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1168,6 +1168,15 @@ func Routes() *web.Route { m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) + m.Group("/tag_protections", func() { + m.Combo("").Get(repo.ListTagProtection). + Post(bind(api.CreateTagProtectionOption{}), mustNotBeArchived, repo.CreateTagProtection) + m.Group("/{id}", func() { + m.Combo("").Get(repo.GetTagProtection). + Patch(bind(api.EditTagProtectionOption{}), mustNotBeArchived, repo.EditTagProtection). + Delete(repo.DeleteTagProtection) + }) + }, reqToken(), reqAdmin()) m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) }, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true)) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 8577a0e896..f72034950f 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -7,9 +7,13 @@ import ( "errors" "fmt" "net/http" + "strings" "code.gitea.io/gitea/models" + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" @@ -287,3 +291,349 @@ func DeleteTag(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +// ListTagProtection lists tag protections for a repo +func ListTagProtection(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/tag_protections repository repoListTagProtection + // --- + // summary: List tag protections for a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/TagProtectionList" + + repo := ctx.Repo.Repository + pts, err := git_model.GetProtectedTags(ctx, repo.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedTags", err) + return + } + apiPts := make([]*api.TagProtection, len(pts)) + for i := range pts { + apiPts[i] = convert.ToTagProtection(ctx, pts[i], repo) + } + + ctx.JSON(http.StatusOK, apiPts) +} + +// GetTagProtection gets a tag protection +func GetTagProtection(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/tag_protections/{id} repository repoGetTagProtection + // --- + // summary: Get a specific tag protection for the repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of the tag protect to get + // type: integer + // required: true + // responses: + // "200": + // "$ref": "#/responses/TagProtection" + // "404": + // "$ref": "#/responses/notFound" + + repo := ctx.Repo.Repository + id := ctx.ParamsInt64(":id") + pt, err := git_model.GetProtectedTagByID(ctx, id) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + return + } + + if pt == nil || repo.ID != pt.RepoID { + ctx.NotFound() + return + } + + ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo)) +} + +// CreateTagProtection creates a tag protection for a repo +func CreateTagProtection(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/tag_protections repository repoCreateTagProtection + // --- + // summary: Create a tag protections for a repository + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateTagProtectionOption" + // responses: + // "201": + // "$ref": "#/responses/TagProtection" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + form := web.GetForm(ctx).(*api.CreateTagProtectionOption) + repo := ctx.Repo.Repository + + namePattern := strings.TrimSpace(form.NamePattern) + if namePattern == "" { + ctx.Error(http.StatusBadRequest, "name_pattern are empty", "name_pattern are empty") + return + } + + if len(form.WhitelistUsernames) == 0 && len(form.WhitelistTeams) == 0 { + ctx.Error(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty", "both whitelist_usernames and whitelist_teams are empty") + return + } + + pt, err := git_model.GetProtectedTagByNamePattern(ctx, repo.ID, namePattern) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectTagOfRepo", err) + return + } else if pt != nil { + ctx.Error(http.StatusForbidden, "Create tag protection", "Tag protection already exist") + return + } + + var whitelistUsers, whitelistTeams []int64 + whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + + if repo.Owner.IsOrganization() { + whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + } + + protectTag := &git_model.ProtectedTag{ + RepoID: repo.ID, + NamePattern: strings.TrimSpace(namePattern), + AllowlistUserIDs: whitelistUsers, + AllowlistTeamIDs: whitelistTeams, + } + if err := git_model.InsertProtectedTag(ctx, protectTag); err != nil { + ctx.Error(http.StatusInternalServerError, "InsertProtectedTag", err) + return + } + + pt, err = git_model.GetProtectedTagByID(ctx, protectTag.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + return + } + + if pt == nil || pt.RepoID != repo.ID { + ctx.Error(http.StatusInternalServerError, "New tag protection not found", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToTagProtection(ctx, pt, repo)) +} + +// EditTagProtection edits a tag protection for a repo +func EditTagProtection(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo}/tag_protections/{id} repository repoEditTagProtection + // --- + // summary: Edit a tag protections for a repository. Only fields that are set will be changed + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of protected tag + // type: integer + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditTagProtectionOption" + // responses: + // "200": + // "$ref": "#/responses/TagProtection" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + // "423": + // "$ref": "#/responses/repoArchivedError" + + repo := ctx.Repo.Repository + form := web.GetForm(ctx).(*api.EditTagProtectionOption) + + id := ctx.ParamsInt64(":id") + pt, err := git_model.GetProtectedTagByID(ctx, id) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + return + } + + if pt == nil || pt.RepoID != repo.ID { + ctx.NotFound() + return + } + + if form.NamePattern != nil { + pt.NamePattern = *form.NamePattern + } + + var whitelistUsers, whitelistTeams []int64 + if form.WhitelistTeams != nil { + if repo.Owner.IsOrganization() { + whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + return + } + } + pt.AllowlistTeamIDs = whitelistTeams + } + + if form.WhitelistUsernames != nil { + whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + return + } + ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + return + } + pt.AllowlistUserIDs = whitelistUsers + } + + err = git_model.UpdateProtectedTag(ctx, pt) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProtectedTag", err) + return + } + + pt, err = git_model.GetProtectedTagByID(ctx, id) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + return + } + + if pt == nil || pt.RepoID != repo.ID { + ctx.Error(http.StatusInternalServerError, "New tag protection not found", "New tag protection not found") + return + } + + ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo)) +} + +// DeleteTagProtection +func DeleteTagProtection(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/tag_protections/{id} repository repoDeleteTagProtection + // --- + // summary: Delete a specific tag protection for the repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of protected tag + // type: integer + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + repo := ctx.Repo.Repository + id := ctx.ParamsInt64(":id") + pt, err := git_model.GetProtectedTagByID(ctx, id) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + return + } + + if pt == nil || pt.RepoID != repo.ID { + ctx.NotFound() + return + } + + err = git_model.DeleteProtectedTag(ctx, pt) + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProtectedTag", err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index cd551cbdfa..1de58632d5 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -170,6 +170,12 @@ type swaggerParameterBodies struct { // in:body CreateTagOption api.CreateTagOption + // in:body + CreateTagProtectionOption api.CreateTagProtectionOption + + // in:body + EditTagProtectionOption api.EditTagProtectionOption + // in:body CreateAccessTokenOption api.CreateAccessTokenOption diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index fcd34a63a9..345835f9a5 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -70,6 +70,20 @@ type swaggerResponseAnnotatedTag struct { Body api.AnnotatedTag `json:"body"` } +// TagProtectionList +// swagger:response TagProtectionList +type swaggerResponseTagProtectionList struct { + // in:body + Body []api.TagProtection `json:"body"` +} + +// TagProtection +// swagger:response TagProtection +type swaggerResponseTagProtection struct { + // in:body + Body api.TagProtection `json:"body"` +} + // Reference // swagger:response Reference type swaggerResponseReference struct { diff --git a/services/convert/convert.go b/services/convert/convert.go index c44179632e..5db33ad85d 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -408,6 +408,32 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api. } } +// ToTagProtection convert a git.ProtectedTag to an api.TagProtection +func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection { + readers, err := access_model.GetRepoReaders(ctx, repo) + if err != nil { + log.Error("GetRepoReaders: %v", err) + } + + whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs) + + teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) + if err != nil { + log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) + } + + whitelistTeams := getWhitelistEntities(teamReaders, pt.AllowlistTeamIDs) + + return &api.TagProtection{ + ID: pt.ID, + NamePattern: pt.NamePattern, + WhitelistUsernames: whitelistUsernames, + WhitelistTeams: whitelistTeams, + Created: pt.CreatedUnix.AsTime(), + Updated: pt.UpdatedUnix.AsTime(), + } +} + // ToTopicResponse convert from models.Topic to api.TopicResponse func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse { return &api.TopicResponse{ diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 09efbd4aa1..ebfdcb6a8f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -13797,6 +13797,233 @@ } } }, + "/repos/{owner}/{repo}/tag_protections": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "List tag protections for a repository", + "operationId": "repoListTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/TagProtectionList" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a tag protections for a repository", + "operationId": "repoCreateTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateTagProtectionOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/TagProtection" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/repos/{owner}/{repo}/tag_protections/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a specific tag protection for the repository", + "operationId": "repoGetTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of the tag protect to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/TagProtection" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete a specific tag protection for the repository", + "operationId": "repoDeleteTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of protected tag", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Edit a tag protections for a repository. Only fields that are set will be changed", + "operationId": "repoEditTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of protected tag", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditTagProtectionOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/TagProtection" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/repos/{owner}/{repo}/tags": { "get": { "produces": [ @@ -19954,6 +20181,31 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateTagProtectionOption": { + "description": "CreateTagProtectionOption options for creating a tag protection", + "type": "object", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + }, + "whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistUsernames" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreateTeamOption": { "description": "CreateTeamOption options for creating a team", "type": "object", @@ -20870,6 +21122,31 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "EditTagProtectionOption": { + "description": "EditTagProtectionOption options for editing a tag protection", + "type": "object", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + }, + "whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistUsernames" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "EditTeamOption": { "description": "EditTeamOption options for editing a team", "type": "object", @@ -24024,6 +24301,46 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "TagProtection": { + "description": "TagProtection represents a tag protection", + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + }, + "whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistUsernames" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Team": { "description": "Team represents a team in an organization", "type": "object", @@ -25635,6 +25952,21 @@ } } }, + "TagProtection": { + "description": "TagProtection", + "schema": { + "$ref": "#/definitions/TagProtection" + } + }, + "TagProtectionList": { + "description": "TagProtectionList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/TagProtection" + } + } + }, "TasksList": { "description": "TasksList", "schema": { From 23147494a789d77cbf187d3ddcca44b772370f26 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 15 Jun 2024 00:26:00 +0000 Subject: [PATCH 07/17] [skip ci] Updated translations via Crowdin --- options/locale/locale_cs-CZ.ini | 7 +++++++ options/locale/locale_pt-PT.ini | 3 +++ 2 files changed, 10 insertions(+) diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 4cbac49ddc..10c5a0fc5d 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1238,6 +1238,7 @@ file_view_rendered=Zobrazit vykreslené file_view_raw=Zobrazit v surovém stavu file_permalink=Trvalý odkaz file_too_large=Soubor je příliš velký pro zobrazení. +file_is_empty=Soubor je prázdný. code_preview_line_from_to=Řádky %[1]d do%[2]d v %[3]s code_preview_line_in=Řádek %[1]d v %[2]s invisible_runes_header=`Tento soubor obsahuje neviditelné znaky Unicode` @@ -1323,6 +1324,7 @@ editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. Klikněte zde, abyste je zobrazili, nebo potvrďte změny ještě jednou pro jejich přepsání. editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři. editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi začal/a s úpravami. Odevzdat do záplatové větve a poté sloučit. +editor.push_out_of_date=Nahrání se zdá být zastaralé. editor.commit_empty_file_header=Odevzdat prázdný soubor editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat? editor.no_changes_to_show=Žádné změny k zobrazení. @@ -2080,6 +2082,7 @@ settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře? settings.mirror_settings.docs.doc_link_pull_section=sekci "stahovat ze vzdáleného úložiště" v dokumentaci. settings.mirror_settings.docs.pulling_remote_title=Stažení ze vzdáleného úložiště settings.mirror_settings.mirrored_repository=Zrcadlený repozitář +settings.mirror_settings.pushed_repository=Odeslaný repozitář settings.mirror_settings.direction=Směr settings.mirror_settings.direction.pull=Natáhnout settings.mirror_settings.direction.push=Nahrát @@ -3284,6 +3287,7 @@ monitor.queue.name=Název monitor.queue.type=Typ monitor.queue.exemplar=Typ vzoru monitor.queue.numberworkers=Počet workerů +monitor.queue.activeworkers=Aktivní workery monitor.queue.maxnumberworkers=Maximální počet workerů monitor.queue.numberinqueue=Číslo ve frontě monitor.queue.review_add=Posoudit / přidat workery @@ -3593,6 +3597,8 @@ status.cancelled=Zrušeno status.skipped=Přeskočeno status.blocked=Blokováno +runners=Runnery +runners.runner_manage_panel=Správa runnerů runners.new=Vytvořit nový runner runners.new_notice=Jak spustit runner runners.status=Status @@ -3619,6 +3625,7 @@ runners.delete_runner_success=Runner byl úspěšně odstraněn runners.delete_runner_failed=Odstranění runneru selhalo runners.delete_runner_header=Potvrdit odstranění tohoto runneru runners.delete_runner_notice=Pokud na tomto runneru běží úloha, bude ukončena a označena jako neúspěšná. Může dojít k přerušení vytváření pracovního postupu. +runners.none=Žádné runnery nejsou k dispozici runners.status.unspecified=Neznámý runners.status.idle=Nečinný runners.status.active=Aktivní diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index f444cf6072..bf7bd492f8 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1238,6 +1238,7 @@ file_view_rendered=Ver resultado processado file_view_raw=Ver em bruto file_permalink=Ligação permanente file_too_large=O ficheiro é demasiado grande para ser apresentado. +file_is_empty=O ficheiro está vazio. code_preview_line_from_to=Linhas %[1]d até %[2]d em %[3]s code_preview_line_in=Linha %[1]d em %[2]s invisible_runes_header=`Este ficheiro contém caracteres Unicode invisíveis` @@ -1554,7 +1555,9 @@ 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=Fechar com comentário issues.reopen_issue=Reabrir +issues.reopen_comment_issue=Reabrir com comentário 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 %[2]s` From 84cbb6c4d2ad57dc8816c0320eac061f753b50c1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 15 Jun 2024 11:43:57 +0800 Subject: [PATCH 08/17] Fix duplicate sub-path for avatars (#31365) Fix #31361, and add tests And this PR introduces an undocumented & debug-purpose-only config option: `USE_SUB_URL_PATH`. It does nothing for end users, it only helps the development of sub-path related problems. And also fix #31366 Co-authored-by: @ExplodingDragon --- custom/conf/app.example.ini | 4 ++ models/repo/avatar_test.go | 28 +++++++++++++ models/user/avatar.go | 6 ++- models/user/avatar_test.go | 28 +++++++++++++ modules/httplib/url.go | 28 +++++++++---- modules/httplib/url_test.go | 10 ++--- modules/setting/global.go | 18 +++++++++ modules/setting/server.go | 11 +++-- modules/setting/setting.go | 5 --- routers/api/packages/container/container.go | 2 +- routers/common/middleware.go | 45 ++++++++++++++++----- routers/common/middleware_test.go | 2 +- services/pull/merge.go | 3 +- 13 files changed, 150 insertions(+), 40 deletions(-) create mode 100644 models/repo/avatar_test.go create mode 100644 models/user/avatar_test.go create mode 100644 modules/setting/global.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index e619aae729..9196180d81 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -81,6 +81,10 @@ RUN_USER = ; git ;; Overwrite the automatically generated public URL. Necessary for proxies and docker. ;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ;; +;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. +;; DO NOT USE IT IN PRODUCTION!!! +;USE_SUB_URL_PATH = false +;; ;; when STATIC_URL_PREFIX is empty it will follow ROOT_URL ;STATIC_URL_PREFIX = ;; diff --git a/models/repo/avatar_test.go b/models/repo/avatar_test.go new file mode 100644 index 0000000000..fc1f8baeca --- /dev/null +++ b/models/repo/avatar_test.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRepoAvatarLink(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost/")() + defer test.MockVariableValue(&setting.AppSubURL, "")() + + repo := &Repository{ID: 1, Avatar: "avatar.png"} + link := repo.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/repo-avatars/avatar.png", link) + + setting.AppURL = "https://localhost/sub-path/" + setting.AppSubURL = "/sub-path" + link = repo.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/sub-path/repo-avatars/avatar.png", link) +} diff --git a/models/user/avatar.go b/models/user/avatar.go index 921bc1b1a1..5453c78fc6 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -89,9 +89,11 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size) } -// 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. +// TODO: refactor it to a relative URL, but it is still used in API response at the moment func (u *User) AvatarLink(ctx context.Context) string { - return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0)) + relLink := u.AvatarLinkWithSize(ctx, 0) // it can't be empty + return httplib.MakeAbsoluteURL(ctx, relLink) } // IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go new file mode 100644 index 0000000000..1078875ee1 --- /dev/null +++ b/models/user/avatar_test.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestUserAvatarLink(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost/")() + defer test.MockVariableValue(&setting.AppSubURL, "")() + + u := &User{ID: 1, Avatar: "avatar.png"} + link := u.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/avatars/avatar.png", link) + + setting.AppURL = "https://localhost/sub-path/" + setting.AppSubURL = "/sub-path" + link = u.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link) +} diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 8dc5b71181..219dfe695c 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -57,11 +57,16 @@ func getForwardedHost(req *http.Request) string { return req.Header.Get("X-Forwarded-Host") } -// GuessCurrentAppURL tries to guess the current full URL by http headers. It always has a '/' suffix, exactly the same as setting.AppURL +// GuessCurrentAppURL tries to guess the current full app URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL func GuessCurrentAppURL(ctx context.Context) string { + return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/" +} + +// GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash. +func GuessCurrentHostURL(ctx context.Context) string { req, ok := ctx.Value(RequestContextKey).(*http.Request) if !ok { - return setting.AppURL + return strings.TrimSuffix(setting.AppURL, 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. @@ -74,20 +79,27 @@ func GuessCurrentAppURL(ctx context.Context) string { // 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 strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/") } reqHost := getForwardedHost(req) if reqHost == "" { reqHost = req.Host } - return reqScheme + "://" + reqHost + setting.AppSubURL + "/" + return reqScheme + "://" + reqHost } -func MakeAbsoluteURL(ctx context.Context, s string) string { - if IsRelativeURL(s) { - return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/") +// MakeAbsoluteURL tries to make a link to an absolute URL: +// * If link is empty, it returns the current app URL. +// * If link is absolute, it returns the link. +// * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed. +func MakeAbsoluteURL(ctx context.Context, link string) string { + if link == "" { + return GuessCurrentAppURL(ctx) } - return s + if !IsRelativeURL(link) { + return link + } + return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/") } func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool { diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index 9980cb74e8..28aaee6e12 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -46,14 +46,14 @@ func TestMakeAbsoluteURL(t *testing.T) { ctx := context.Background() 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://cfg-host/foo", MakeAbsoluteURL(ctx, "foo")) + assert.Equal(t, "http://cfg-host/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://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/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, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo")) ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ Host: "user-host", @@ -70,7 +70,7 @@ func TestMakeAbsoluteURL(t *testing.T) { "X-Forwarded-Proto": {"https"}, }, }) - assert.Equal(t, "https://forwarded-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "https://forwarded-host/foo", MakeAbsoluteURL(ctx, "/foo")) } func TestIsCurrentGiteaSiteURL(t *testing.T) { diff --git a/modules/setting/global.go b/modules/setting/global.go new file mode 100644 index 0000000000..55dfe485b2 --- /dev/null +++ b/modules/setting/global.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +// Global settings +var ( + // RunUser is the OS user that Gitea is running as. ini:"RUN_USER" + RunUser string + // RunMode is the running mode of Gitea, it only accepts two values: "dev" and "prod". + // Non-dev values will be replaced by "prod". ini: "RUN_MODE" + RunMode string + // IsProd is true if RunMode is not "dev" + IsProd bool + + // AppName is the Application name, used in the page title. ini: "APP_NAME" + AppName string +) diff --git a/modules/setting/server.go b/modules/setting/server.go index 7d6ece2727..d7a71578d4 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -40,16 +40,16 @@ const ( LandingPageLogin LandingPage = "/user/login" ) +// Server settings var ( - // AppName is the Application name, used in the page title. - // It maps to ini:"APP_NAME" - AppName string // AppURL is the Application ROOT_URL. It always has a '/' suffix // It maps to ini:"ROOT_URL" AppURL string // AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'. // This value is empty if site does not have sub-url. AppSubURL string + // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems without a reverse proxy. + UseSubURLPath bool // AppDataPath is the default path for storing data. // It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data" AppDataPath string @@ -59,8 +59,6 @@ var ( // AssetVersion holds a opaque value that is used for cache-busting assets AssetVersion string - // Server settings - Protocol Scheme UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` @@ -275,9 +273,10 @@ func loadServerFrom(rootCfg ConfigProvider) { // This should be TrimRight to ensure that there is only a single '/' at the end of AppURL. AppURL = strings.TrimRight(appURL.String(), "/") + "/" - // Suburl should start with '/' and end without '/', such as '/{subpath}'. + // AppSubURL should start with '/' and end without '/', such as '/{subpath}'. // This value is empty if site does not have sub-url. AppSubURL = strings.TrimSuffix(appURL.Path, "/") + UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false) StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/") // Check if Domain differs from AppURL domain than update it to AppURL's domain diff --git a/modules/setting/setting.go b/modules/setting/setting.go index f056fbfc6c..b4f913cdae 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -25,12 +25,7 @@ var ( // AppStartTime store time gitea has started AppStartTime time.Time - // Other global setting objects - CfgProvider ConfigProvider - RunMode string - RunUser string - IsProd bool IsWindows bool // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index b0c4458d51..5007037bee 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -117,7 +117,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) { func apiUnauthorizedError(ctx *context.Context) { // 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" + realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token" ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`) apiErrorDefined(ctx, errUnauthorized) } diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 8b661993bb..de49648396 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -25,7 +25,7 @@ import ( // ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery func ProtocolMiddlewares() (handlers []any) { // first, normalize the URL path - handlers = append(handlers, stripSlashesMiddleware) + handlers = append(handlers, normalizeRequestPathMiddleware) // prepare the ContextData and panic recovery handlers = append(handlers, func(next http.Handler) http.Handler { @@ -75,9 +75,9 @@ func ProtocolMiddlewares() (handlers []any) { return handlers } -func stripSlashesMiddleware(next http.Handler) http.Handler { +func normalizeRequestPathMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - // First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL + // escape the URL RawPath to ensure that all routing is done using a correctly escaped URL req.URL.RawPath = req.URL.EscapedPath() urlPath := req.URL.RawPath @@ -86,19 +86,42 @@ func stripSlashesMiddleware(next http.Handler) http.Handler { urlPath = rctx.RoutePath } - sanitizedPath := &strings.Builder{} - prevWasSlash := false - for _, chr := range strings.TrimRight(urlPath, "/") { - if chr != '/' || !prevWasSlash { - sanitizedPath.WriteRune(chr) + normalizedPath := strings.TrimRight(urlPath, "/") + // the following code block is a slow-path for replacing all repeated slashes "//" to one single "/" + // if the path doesn't have repeated slashes, then no need to execute it + if strings.Contains(normalizedPath, "//") { + buf := &strings.Builder{} + prevWasSlash := false + for _, chr := range normalizedPath { + if chr != '/' || !prevWasSlash { + buf.WriteRune(chr) + } + prevWasSlash = chr == '/' } - prevWasSlash = chr == '/' + normalizedPath = buf.String() + } + + if setting.UseSubURLPath { + remainingPath, ok := strings.CutPrefix(normalizedPath, setting.AppSubURL+"/") + if ok { + normalizedPath = "/" + remainingPath + } else if normalizedPath == setting.AppSubURL { + normalizedPath = "/" + } else if !strings.HasPrefix(normalizedPath+"/", "/v2/") { + // do not respond to other requests, to simulate a real sub-path environment + http.Error(resp, "404 page not found, sub-path is: "+setting.AppSubURL, http.StatusNotFound) + return + } + // TODO: it's not quite clear about how req.URL and rctx.RoutePath work together. + // Fortunately, it is only used for debug purpose, we have enough time to figure it out in the future. + req.URL.RawPath = normalizedPath + req.URL.Path = normalizedPath } if rctx == nil { - req.URL.Path = sanitizedPath.String() + req.URL.Path = normalizedPath } else { - rctx.RoutePath = sanitizedPath.String() + rctx.RoutePath = normalizedPath } next.ServeHTTP(resp, req) }) diff --git a/routers/common/middleware_test.go b/routers/common/middleware_test.go index f16b9374ec..c96071c3a8 100644 --- a/routers/common/middleware_test.go +++ b/routers/common/middleware_test.go @@ -61,7 +61,7 @@ func TestStripSlashesMiddleware(t *testing.T) { }) // pass the test middleware to validate the changes - handlerToTest := stripSlashesMiddleware(testMiddleware) + handlerToTest := normalizeRequestPathMiddleware(testMiddleware) // create a mock request to use req := httptest.NewRequest("GET", tt.inputPath, nil) // call the handler using a mock response recorder diff --git a/services/pull/merge.go b/services/pull/merge.go index 9ef3fb2e05..e19292c31c 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -23,6 +23,7 @@ import ( 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/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" repo_module "code.gitea.io/gitea/modules/repository" @@ -56,7 +57,7 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue issueReference = "!" } - reviewedOn := fmt.Sprintf("Reviewed-on: %s/%s", setting.AppURL, pr.Issue.Link()) + reviewedOn := fmt.Sprintf("Reviewed-on: %s", httplib.MakeAbsoluteURL(ctx, pr.Issue.Link())) reviewedBy := pr.GetApprovers(ctx) if mergeStyle != "" { From 42718d32af9d259205bee0fde818ffc0c3a9797f Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Sat, 15 Jun 2024 12:20:14 +0800 Subject: [PATCH 09/17] Allow downloading attachments of draft releases (#31369) Fix #31362 --- routers/web/repo/repo.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 5a74971827..514a5ab02c 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -418,8 +418,9 @@ func RedirectDownload(ctx *context.Context) { tagNames := []string{vTag} curRepo := ctx.Repo.Repository releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ - RepoID: curRepo.ID, - TagNames: tagNames, + IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases), + RepoID: curRepo.ID, + TagNames: tagNames, }) if err != nil { ctx.ServerError("RedirectDownload", err) From e37ecd17324946d9b2db07ea10d4a9fbb53da20f Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sat, 15 Jun 2024 06:48:52 +0200 Subject: [PATCH 10/17] rm const do inline (#31360) https://github.com/go-gitea/gitea/pull/30876/files#r1637288202 --- models/repo/search.go | 60 +++++++++++++------------------------------ 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/models/repo/search.go b/models/repo/search.go index c500d41be8..2baa85dc6f 100644 --- a/models/repo/search.go +++ b/models/repo/search.go @@ -5,53 +5,29 @@ package repo import "code.gitea.io/gitea/models/db" -// Strings for sorting result -const ( - // only used for repos - SearchOrderByAlphabetically db.SearchOrderBy = "owner_name ASC, name ASC" - SearchOrderByAlphabeticallyReverse db.SearchOrderBy = "owner_name DESC, name DESC" - SearchOrderBySize db.SearchOrderBy = "size ASC" - SearchOrderBySizeReverse db.SearchOrderBy = "size DESC" - SearchOrderByGitSize db.SearchOrderBy = "git_size ASC" - SearchOrderByGitSizeReverse db.SearchOrderBy = "git_size DESC" - SearchOrderByLFSSize db.SearchOrderBy = "lfs_size ASC" - SearchOrderByLFSSizeReverse db.SearchOrderBy = "lfs_size DESC" - // alias as also used elsewhere - SearchOrderByLeastUpdated db.SearchOrderBy = db.SearchOrderByLeastUpdated - SearchOrderByRecentUpdated db.SearchOrderBy = db.SearchOrderByRecentUpdated - SearchOrderByOldest db.SearchOrderBy = db.SearchOrderByOldest - SearchOrderByNewest db.SearchOrderBy = db.SearchOrderByNewest - SearchOrderByID db.SearchOrderBy = db.SearchOrderByID - SearchOrderByIDReverse db.SearchOrderBy = db.SearchOrderByIDReverse - SearchOrderByStars db.SearchOrderBy = db.SearchOrderByStars - SearchOrderByStarsReverse db.SearchOrderBy = db.SearchOrderByStarsReverse - SearchOrderByForks db.SearchOrderBy = db.SearchOrderByForks - SearchOrderByForksReverse db.SearchOrderBy = db.SearchOrderByForksReverse -) - // SearchOrderByMap represents all possible search order var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ "asc": { - "alpha": SearchOrderByAlphabetically, - "created": SearchOrderByOldest, - "updated": SearchOrderByLeastUpdated, - "size": SearchOrderBySize, - "git_size": SearchOrderByGitSize, - "lfs_size": SearchOrderByLFSSize, - "id": SearchOrderByID, - "stars": SearchOrderByStars, - "forks": SearchOrderByForks, + "alpha": "owner_name ASC, name ASC", + "created": db.SearchOrderByOldest, + "updated": db.SearchOrderByLeastUpdated, + "size": "size ASC", + "git_size": "git_size ASC", + "lfs_size": "lfs_size ASC", + "id": db.SearchOrderByID, + "stars": db.SearchOrderByStars, + "forks": db.SearchOrderByForks, }, "desc": { - "alpha": SearchOrderByAlphabeticallyReverse, - "created": SearchOrderByNewest, - "updated": SearchOrderByRecentUpdated, - "size": SearchOrderBySizeReverse, - "git_size": SearchOrderByGitSizeReverse, - "lfs_size": SearchOrderByLFSSizeReverse, - "id": SearchOrderByIDReverse, - "stars": SearchOrderByStarsReverse, - "forks": SearchOrderByForksReverse, + "alpha": "owner_name DESC, name DESC", + "created": db.SearchOrderByNewest, + "updated": db.SearchOrderByRecentUpdated, + "size": "size DESC", + "git_size": "git_size DESC", + "lfs_size": "lfs_size DESC", + "id": db.SearchOrderByIDReverse, + "stars": db.SearchOrderByStarsReverse, + "forks": db.SearchOrderByForksReverse, }, } From 78e8296e113e2fd9259ec05fe87035427821ea0b Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sat, 15 Jun 2024 08:45:02 +0200 Subject: [PATCH 11/17] Rename repo_model.SearchOrderByMap to repo_model.OrderByMap (#31359) https://github.com/go-gitea/gitea/pull/30876#discussion_r1637112394 --- models/repo/search.go | 38 ++++++++++++++++++------------------- routers/api/v1/repo/repo.go | 2 +- routers/web/explore/repo.go | 2 +- routers/web/repo/repo.go | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/models/repo/search.go b/models/repo/search.go index 2baa85dc6f..a73d9fc215 100644 --- a/models/repo/search.go +++ b/models/repo/search.go @@ -5,8 +5,8 @@ package repo import "code.gitea.io/gitea/models/db" -// SearchOrderByMap represents all possible search order -var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ +// OrderByMap represents all possible search order +var OrderByMap = map[string]map[string]db.SearchOrderBy{ "asc": { "alpha": "owner_name ASC, name ASC", "created": db.SearchOrderByOldest, @@ -31,22 +31,22 @@ var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ }, } -// SearchOrderByFlatMap is similar to SearchOrderByMap but use human language keywords +// OrderByFlatMap is similar to OrderByMap but use human language keywords // to decide between asc and desc -var SearchOrderByFlatMap = map[string]db.SearchOrderBy{ - "newest": SearchOrderByMap["desc"]["created"], - "oldest": SearchOrderByMap["asc"]["created"], - "leastupdate": SearchOrderByMap["asc"]["updated"], - "reversealphabetically": SearchOrderByMap["desc"]["alpha"], - "alphabetically": SearchOrderByMap["asc"]["alpha"], - "reversesize": SearchOrderByMap["desc"]["size"], - "size": SearchOrderByMap["asc"]["size"], - "reversegitsize": SearchOrderByMap["desc"]["git_size"], - "gitsize": SearchOrderByMap["asc"]["git_size"], - "reverselfssize": SearchOrderByMap["desc"]["lfs_size"], - "lfssize": SearchOrderByMap["asc"]["lfs_size"], - "moststars": SearchOrderByMap["desc"]["stars"], - "feweststars": SearchOrderByMap["asc"]["stars"], - "mostforks": SearchOrderByMap["desc"]["forks"], - "fewestforks": SearchOrderByMap["asc"]["forks"], +var OrderByFlatMap = map[string]db.SearchOrderBy{ + "newest": OrderByMap["desc"]["created"], + "oldest": OrderByMap["asc"]["created"], + "leastupdate": OrderByMap["asc"]["updated"], + "reversealphabetically": OrderByMap["desc"]["alpha"], + "alphabetically": OrderByMap["asc"]["alpha"], + "reversesize": OrderByMap["desc"]["size"], + "size": OrderByMap["asc"]["size"], + "reversegitsize": OrderByMap["desc"]["git_size"], + "gitsize": OrderByMap["asc"]["git_size"], + "reverselfssize": OrderByMap["desc"]["lfs_size"], + "lfssize": OrderByMap["asc"]["lfs_size"], + "moststars": OrderByMap["desc"]["stars"], + "feweststars": OrderByMap["asc"]["stars"], + "mostforks": OrderByMap["desc"]["forks"], + "fewestforks": OrderByMap["asc"]["forks"], } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 9ba5887525..4f617d27af 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -184,7 +184,7 @@ func Search(ctx *context.APIContext) { if len(sortOrder) == 0 { sortOrder = "asc" } - if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { + if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok { if orderBy, ok := searchModeMap[sortMode]; ok { opts.OrderBy = orderBy } else { diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 22b1fc5bf9..1d17f962f2 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -63,7 +63,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { sortOrder = setting.UI.ExploreDefaultSort } - if order, ok := repo_model.SearchOrderByFlatMap[sortOrder]; ok { + if order, ok := repo_model.OrderByFlatMap[sortOrder]; ok { orderBy = order } else { sortOrder = "recentupdate" diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 514a5ab02c..92f74bbf33 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -616,7 +616,7 @@ func SearchRepo(ctx *context.Context) { if len(sortOrder) == 0 { sortOrder = "asc" } - if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { + if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok { if orderBy, ok := searchModeMap[sortMode]; ok { opts.OrderBy = orderBy } else { From c70d4f03c97dea47c0314bc3153bc448278a9a80 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 16 Jun 2024 00:28:50 +0000 Subject: [PATCH 12/17] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index d85ffb4694..acce73b0af 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1238,6 +1238,7 @@ file_view_rendered=レンダリング表示 file_view_raw=Rawデータを見る file_permalink=パーマリンク file_too_large=このファイルは大きすぎるため、表示できません。 +file_is_empty=ファイルは空です。 code_preview_line_from_to=%[1]d 行目から %[2]d 行目 in %[3]s code_preview_line_in=%[1]d 行目 in %[2]s invisible_runes_header=このファイルには不可視のUnicode文字が含まれています @@ -1442,6 +1443,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=オープン @@ -1757,6 +1759,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=メンテナーからの編集を許可する @@ -1902,6 +1905,7 @@ pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ %[1 pull.deleted_branch=(削除済み):%s +comments.edit.already_changed=コメントの変更を保存できません。 他のユーザーによって内容がすでに変更されているようです。 変更を上書きしないようにするため、ページを更新してからもう一度編集してください milestones.new=新しいマイルストーン milestones.closed=%s にクローズ From f446e3b4ab2d3f787a17d1cfe09ee8f5f7b10089 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 16 Jun 2024 10:07:21 +0800 Subject: [PATCH 13/17] Fix JS error when creating new issue (#31383) Fix #31336 --- web_src/js/features/repo-legacy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index e53d86cca0..de4f611b5d 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -272,7 +272,7 @@ export function initRepoCommentForm() { } $list.find('.selected').html(` - + ${icon} ${htmlEscape(this.textContent)} From 129206da4543f2024601af20dac3eaf978d0c432 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 17 Jun 2024 00:27:39 +0000 Subject: [PATCH 14/17] [skip ci] Updated licenses and gitignores --- options/gitignore/IAR | 47 +++++++++++++++++++++++++++++++++++ options/gitignore/Objective-C | 7 ------ options/gitignore/Terraform | 3 --- 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 options/gitignore/IAR diff --git a/options/gitignore/IAR b/options/gitignore/IAR new file mode 100644 index 0000000000..e8938b31a4 --- /dev/null +++ b/options/gitignore/IAR @@ -0,0 +1,47 @@ +# Compiled binaries +*.o +*.bin +*.elf +*.hex +*.map +*.out +*.obj + +# Trash +*.bak +thumbs.db +*.~* + +# IAR Settings +**/settings/*.crun +**/settings/*.dbgdt +**/settings/*.cspy +**/settings/*.cspy.* +**/settings/*.xcl +**/settings/*.dni +**/settings/*.wsdt +**/settings/*.wspos + +# IAR Debug Exe +**/Exe/*.sim + +# IAR Debug Obj +**/Obj/*.pbd +**/Obj/*.pbd.* +**/Obj/*.pbi +**/Obj/*.pbi.* + +# IAR project "Debug" directory +Debug/ + +# IAR project "Release" directory +Release/ + +# IAR project settings directory +settings/ + +# IAR backup files +Backup* + +# IAR .dep files +*.dep \ No newline at end of file diff --git a/options/gitignore/Objective-C b/options/gitignore/Objective-C index 9b8cd0706f..2ebce16e6e 100644 --- a/options/gitignore/Objective-C +++ b/options/gitignore/Objective-C @@ -42,10 +42,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/Terraform b/options/gitignore/Terraform index 15073ca88b..2faf43d0a1 100644 --- a/options/gitignore/Terraform +++ b/options/gitignore/Terraform @@ -35,6 +35,3 @@ override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc - -# Ignore hcl file -.terraform.lock.hcl From f5dfd7d73cbc606ae65e5bab33efad1604b6331e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 17 Jun 2024 09:18:35 +0800 Subject: [PATCH 15/17] Add a simple test for AdoptRepository (#31391) Follow #31333 --- services/repository/adopt_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go index c1520e01c9..38949c7602 100644 --- a/services/repository/adopt_test.go +++ b/services/repository/adopt_test.go @@ -6,10 +6,13 @@ package repository import ( "os" "path" + "path/filepath" "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -83,3 +86,13 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) { assert.Equal(t, 2, count) assert.Equal(t, unadoptedList[1], repoNames[0]) } + +func TestAdoptRepository(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.NoError(t, unittest.CopyDir(filepath.Join(setting.RepoRootPath, "user2", "repo1.git"), filepath.Join(setting.RepoRootPath, "user2", "test-adopt.git"))) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + _, err := AdoptRepository(db.DefaultContext, user2, user2, CreateRepoOptions{Name: "test-adopt"}) + assert.NoError(t, err) + repoTestAdopt := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "test-adopt"}) + assert.Equal(t, "sha1", repoTestAdopt.ObjectFormatName) +} From 597d1da96b92b181c106813ce26149334b2b44e5 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 17 Jun 2024 08:16:14 +0200 Subject: [PATCH 16/17] Fix missing images in editor preview due to wrong links (#31299) Parse base path and tree path so that media links can be correctly created with /media/. Resolves #31294 --------- Co-authored-by: wxiaoguang --- modules/markup/renderer.go | 8 +-- modules/structs/miscellaneous.go | 6 +- routers/api/v1/misc/markup_test.go | 100 ++++++++++++++++++----------- routers/common/markup.go | 63 +++++++++--------- templates/swagger/v1_json.tmpl | 4 +- 5 files changed, 103 insertions(+), 78 deletions(-) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 66e8cf611d..5eb568cb1f 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -86,10 +86,10 @@ type RenderContext struct { } type Links struct { - AbsolutePrefix bool - Base string - BranchPath string - TreePath string + AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias + Base string // base prefix for pre-provided links and medias (images, videos) + BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0" + TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered } func (l *Links) Prefix() string { diff --git a/modules/structs/miscellaneous.go b/modules/structs/miscellaneous.go index bff10f95b7..3b206c1dd7 100644 --- a/modules/structs/miscellaneous.go +++ b/modules/structs/miscellaneous.go @@ -25,7 +25,8 @@ type MarkupOption struct { // // in: body Mode string - // Context to render + // URL path for rendering issue, media and file links + // Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir} // // in: body Context string @@ -53,7 +54,8 @@ type MarkdownOption struct { // // in: body Mode string - // Context to render + // URL path for rendering issue, media and file links + // Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir} // // in: body Context string diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go index 5236fd06ae..e2ab7141b7 100644 --- a/routers/api/v1/misc/markup_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -7,6 +7,7 @@ import ( go_context "context" "io" "net/http" + "path" "strings" "testing" @@ -19,36 +20,40 @@ import ( "github.com/stretchr/testify/assert" ) -const ( - AppURL = "http://localhost:3000/" - Repo = "gogits/gogs" - FullURL = AppURL + Repo + "/" -) +const AppURL = "http://localhost:3000/" -func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) { +func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) { setting.AppURL = AppURL + context := "/gogits/gogs" + if !wiki { + context += path.Join("/src/branch/main", path.Dir(filePath)) + } options := api.MarkupOption{ Mode: mode, Text: text, - Context: Repo, - Wiki: true, + Context: context, + Wiki: wiki, FilePath: filePath, } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") web.SetForm(ctx, &options) Markup(ctx) - assert.Equal(t, responseBody, resp.Body.String()) - assert.Equal(t, responseCode, resp.Code) + assert.Equal(t, expectedBody, resp.Body.String()) + assert.Equal(t, expectedCode, resp.Code) resp.Body.Reset() } -func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) { +func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) { setting.AppURL = AppURL + context := "/gogits/gogs" + if !wiki { + context += "/src/branch/main" + } options := api.MarkdownOption{ Mode: mode, Text: text, - Context: Repo, - Wiki: true, + Context: context, + Wiki: wiki, } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") web.SetForm(ctx, &options) @@ -65,7 +70,7 @@ func TestAPI_RenderGFM(t *testing.T) { }, }) - testCasesCommon := []string{ + testCasesWiki := []string{ // dear imgui wiki markdown extract: special wiki syntax `Wiki! Enjoy :) - [[Links, Language bindings, Engine bindings|Links]] @@ -74,20 +79,20 @@ func TestAPI_RenderGFM(t *testing.T) { // rendered `

Wiki! Enjoy :)

`, // Guard wiki sidebar: special syntax `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, // rendered - `

Guardfile-DSL / Configuring-Guard

+ `

Guardfile-DSL / Configuring-Guard

`, // special syntax `[[Name|Link]]`, // rendered - `

Name

+ `

Name

`, // empty ``, @@ -95,7 +100,7 @@ func TestAPI_RenderGFM(t *testing.T) { ``, } - testCasesDocument := []string{ + testCasesWikiDocument := []string{ // wine-staging wiki home extract: special wiki syntax, images `## What is Wine Staging? **Wine Staging** on website [wine-staging.com](http://wine-staging.com). @@ -111,31 +116,48 @@ Here are some links to the most important topics. You can find the full list of

Wine Staging on website wine-staging.com.

Here are some links to the most important topics. You can find the full list of pages at the sidebar.

-

Configuration -images/icon-bug.png

+

Configuration +images/icon-bug.png

`, } - for i := 0; i < len(testCasesCommon); i += 2 { - text := testCasesCommon[i] - response := testCasesCommon[i+1] - testRenderMarkdown(t, "gfm", text, response, http.StatusOK) - testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) - testRenderMarkdown(t, "comment", text, response, http.StatusOK) - testRenderMarkup(t, "comment", "", text, response, http.StatusOK) - testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) + for i := 0; i < len(testCasesWiki); i += 2 { + text := testCasesWiki[i] + response := testCasesWiki[i+1] + testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK) + testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK) + testRenderMarkdown(t, "comment", true, text, response, http.StatusOK) + testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK) + testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK) } - for i := 0; i < len(testCasesDocument); i += 2 { - text := testCasesDocument[i] - response := testCasesDocument[i+1] - testRenderMarkdown(t, "gfm", text, response, http.StatusOK) - testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) - testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) + for i := 0; i < len(testCasesWikiDocument); i += 2 { + text := testCasesWikiDocument[i] + response := testCasesWikiDocument[i+1] + testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK) + testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK) + testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK) } - testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) - testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) + input := "[Link](test.md)\n![Image](image.png)" + testRenderMarkdown(t, "gfm", false, input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkdown(t, "gfm", false, input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkup(t, "gfm", false, "", input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkup(t, "file", false, "path/new-file.md", input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkup(t, "file", true, "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) + testRenderMarkup(t, "unknown", true, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) } var simpleCases = []string{ @@ -160,7 +182,7 @@ func TestAPI_RenderSimple(t *testing.T) { options := api.MarkdownOption{ Mode: "markdown", Text: "", - Context: Repo, + Context: "/gogits/gogs", } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") for i := 0; i < len(simpleCases); i += 2 { diff --git a/routers/common/markup.go b/routers/common/markup.go index f7d096008a..0a00eac7d4 100644 --- a/routers/common/markup.go +++ b/routers/common/markup.go @@ -7,63 +7,67 @@ package common import ( "fmt" "net/http" + "path" "strings" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" - - "mvdan.cc/xurls/v2" ) // RenderMarkup renders markup text for the /markup and /markdown endpoints -func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPrefix, filePath string, wiki bool) { - var markupType string - relativePath := "" +func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) { + // urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}" + // filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file") + // filePath will be used as RenderContext.RelativePath - if len(text) == 0 { - _, _ = ctx.Write([]byte("")) - return + // for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md" + // and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc" + + var markupType, relativePath string + + links := markup.Links{AbsolutePrefix: true} + if urlPathContext != "" { + links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext) } switch mode { case "markdown": // Raw markdown if err := markdown.RenderRaw(&markup.RenderContext{ - Ctx: ctx, - Links: markup.Links{ - AbsolutePrefix: true, - Base: urlPrefix, - }, + Ctx: ctx, + Links: links, }, strings.NewReader(text), ctx.Resp); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) } return case "comment": - // Comment as markdown + // Issue & comment content markupType = markdown.MarkupName case "gfm": - // Github Flavored Markdown as document + // GitHub Flavored Markdown markupType = markdown.MarkupName case "file": - // File as document based on file extension - markupType = "" + markupType = "" // render the repo file content by its extension relativePath = filePath default: ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) return } - if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { - // check if urlPrefix is already set to a URL - linkRegex, _ := xurls.StrictMatchingScheme("https?://") - m := linkRegex.FindStringIndex(urlPrefix) - if m == nil { - urlPrefix = util.URLJoin(setting.AppURL, urlPrefix) - } + fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5) + if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") { + // absolute base prefix is something like "https://host/subpath/{user}/{repo}" + absoluteBasePrefix := fmt.Sprintf("%s%s/%s", httplib.GuessCurrentAppURL(ctx), fields[0], fields[1]) + + fileDir := path.Dir(filePath) // it is "doc" if filePath is "doc/CHANGE.md" + refPath := strings.Join(fields[3:], "/") // it is "branch/features/feat-12/doc" + refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12" + + links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir} } meta := map[string]string{} @@ -81,12 +85,9 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr } if err := markup.Render(&markup.RenderContext{ - Ctx: ctx, - Repo: repoCtx, - Links: markup.Links{ - AbsolutePrefix: true, - Base: urlPrefix, - }, + Ctx: ctx, + Repo: repoCtx, + Links: links, Metas: meta, IsWiki: wiki, Type: markupType, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index ebfdcb6a8f..4aa64c5376 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -22404,7 +22404,7 @@ "type": "object", "properties": { "Context": { - "description": "Context to render\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", "type": "string" }, "Mode": { @@ -22427,7 +22427,7 @@ "type": "object", "properties": { "Context": { - "description": "Context to render\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", "type": "string" }, "FilePath": { From 25f3ec5b65ef00fb2cf584a37c5981e674459575 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 17 Jun 2024 14:45:12 +0800 Subject: [PATCH 17/17] Fix natural sort (#31384) Fix #31374 --- modules/base/natural_sort.go | 57 ++++++++++++++++++++++++++++++- modules/base/natural_sort_test.go | 43 +++++++++++++++-------- 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go index 0f90ec70ce..acb9002276 100644 --- a/modules/base/natural_sort.go +++ b/modules/base/natural_sort.go @@ -4,12 +4,67 @@ package base import ( + "unicode/utf8" + "golang.org/x/text/collate" "golang.org/x/text/language" ) +func naturalSortGetRune(str string, pos int) (r rune, size int, has bool) { + if pos >= len(str) { + return 0, 0, false + } + r, size = utf8.DecodeRuneInString(str[pos:]) + if r == utf8.RuneError { + r, size = rune(str[pos]), 1 // if invalid input, treat it as a single byte ascii + } + return r, size, true +} + +func naturalSortAdvance(str string, pos int) (end int, isNumber bool) { + end = pos + for { + r, size, has := naturalSortGetRune(str, end) + if !has { + break + } + isCurRuneNum := '0' <= r && r <= '9' + if end == pos { + isNumber = isCurRuneNum + end += size + } else if isCurRuneNum == isNumber { + end += size + } else { + break + } + } + return end, isNumber +} + // NaturalSortLess compares two strings so that they could be sorted in natural order func NaturalSortLess(s1, s2 string) bool { + // There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997 + // text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997 + // So we need to handle the number parts by ourselves c := collate.New(language.English, collate.Numeric) - return c.CompareString(s1, s2) < 0 + pos1, pos2 := 0, 0 + for pos1 < len(s1) && pos2 < len(s2) { + end1, isNum1 := naturalSortAdvance(s1, pos1) + end2, isNum2 := naturalSortAdvance(s2, pos2) + part1, part2 := s1[pos1:end1], s2[pos2:end2] + if isNum1 && isNum2 { + if part1 != part2 { + if len(part1) != len(part2) { + return len(part1) < len(part2) + } + return part1 < part2 + } + } else { + if cmp := c.CompareString(part1, part2); cmp != 0 { + return cmp < 0 + } + } + pos1, pos2 = end1, end2 + } + return len(s1) < len(s2) } diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go index f27a4eb53a..b001bc4ac9 100644 --- a/modules/base/natural_sort_test.go +++ b/modules/base/natural_sort_test.go @@ -10,21 +10,36 @@ import ( ) func TestNaturalSortLess(t *testing.T) { - test := func(s1, s2 string, less bool) { - assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2) + testLess := func(s1, s2 string) { + assert.True(t, NaturalSortLess(s1, s2), "s1