From 2c341b6803622772619621e2620755a0ddce07e8 Mon Sep 17 00:00:00 2001
From: GiteaBot
Date: Tue, 10 Jun 2025 00:37:13 +0000
Subject: [PATCH 001/131] [skip ci] Updated translations via Crowdin
---
options/locale/locale_de-DE.ini | 77 ++++++
options/locale/locale_uk-UA.ini | 449 ++++++++++++++++++++++++++++++--
options/locale/locale_zh-CN.ini | 1 +
3 files changed, 509 insertions(+), 18 deletions(-)
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index f115dee247..56dcadd451 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -113,9 +113,11 @@ copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden
write=Verfassen
preview=Vorschau
loading=Laden…
+files=Dateien
error=Fehler
error404=Die Seite, die Du versuchst aufzurufen, existiert nicht oder Du bist nicht berechtigt, diese anzusehen.
+error503=Der Server konnte deine Anfrage nicht abschließen. Bitte versuche es später erneut.
go_back=Zurück
invalid_data=Ungültige Daten: %v
@@ -128,6 +130,7 @@ pin=Anheften
unpin=Loslösen
artifacts=Artefakte
+expired=Abgelaufen
confirm_delete_artifact=Bist du sicher, dass du das Artefakt '%s' löschen möchtest?
archived=Archiviert
@@ -169,6 +172,10 @@ search=Suche ...
type_tooltip=Suchmodus
fuzzy=Ähnlich
fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind
+words=Wörter
+words_tooltip=Nur Suchbegriffe einbeziehen, die den Suchbegriffen exakt entsprechen
+regexp=Regexp
+regexp_tooltip=Nur Suchbegriffe einbeziehen, die dem Regexp exakt entsprechen
exact=Exakt
exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen
repo_kind=Repositories durchsuchen ...
@@ -444,6 +451,7 @@ use_scratch_code=Einmalpasswort verwenden
twofa_scratch_used=Du hast dein Einmalpasswort verwendet. Du wurdest zu den Einstellung der Zwei-Faktor-Authentifizierung umgeleitet, dort kannst du dein Gerät abmelden oder ein neues Einmalpasswort erzeugen.
twofa_passcode_incorrect=Ungültige PIN. Wenn du dein Gerät verloren hast, verwende dein Einmalpasswort.
twofa_scratch_token_incorrect=Das Einmalpasswort ist falsch.
+twofa_required=Du musst die Zwei-Faktor-Authentifizierung einrichten, um Zugriff auf die Repositories zu erhalten, oder versuche dich erneut anzumelden.
login_userpass=Anmelden
login_openid=OpenID
oauth_signup_tab=Neues Konto registrieren
@@ -452,6 +460,7 @@ oauth_signup_submit=Konto vervollständigen
oauth_signin_tab=Mit existierendem Konto verbinden
oauth_signin_title=Anmelden um verbundenes Konto zu autorisieren
oauth_signin_submit=Konto verbinden
+oauth.signin.error.general=Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten: %s. Wenn dieser Fehler weiterhin besteht, wende dich bitte an den Administrator.
oauth.signin.error.access_denied=Die Autorisierungsanfrage wurde abgelehnt.
oauth.signin.error.temporarily_unavailable=Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut.
oauth_callback_unable_auto_reg=Automatische Registrierung ist aktiviert, aber der OAuth2-Provider %[1]s hat fehlende Felder zurückgegeben: %[2]s, kann den Account nicht automatisch erstellen. Bitte erstelle oder verbinde einen Account oder kontaktieren den Administrator.
@@ -724,6 +733,8 @@ public_profile=Öffentliches Profil
biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden)
location_placeholder=Teile Deinen ungefähren Standort mit anderen
profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet.
+password_username_disabled=Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.
+password_full_name_disabled=Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.
full_name=Vollständiger Name
website=Webseite
location=Standort
@@ -918,6 +929,9 @@ permission_not_set=Nicht festgelegt
permission_no_access=Kein Zugriff
permission_read=Lesen
permission_write=Lesen und Schreiben
+permission_anonymous_read=Anonymes Lesen
+permission_everyone_read=Alle lesen
+permission_everyone_write=Alle schreiben
access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden API-Routen. Lies die Dokumentation für mehr Informationen.
at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen
permissions_list=Berechtigungen:
@@ -1020,6 +1034,9 @@ new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des
owner=Besitzer
owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist.
repo_name=Repository-Name
+repo_name_profile_public_hint=.profile ist ein spezielles Repository, mit dem du die README.md zu deinem öffentlichen Organisationsprofil hinzufügen kannst, das für jeden sichtbar ist. Stelle sicher, dass es öffentlich ist und initialisiere es mit einer README im Profilverzeichnis, um loszulegen.
+repo_name_profile_private_hint=.profile ist ein spezielles Repository, mit dem du die README.md zu deinem privaten Organisationsprofil hinzufügen kannst, das nur für Organisationsmitglieder sichtbar ist. Stelle sicher, dass es privat ist und initialisiere es mit einer README im Profilverzeichnis, um loszulegen.
+repo_name_helper=Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern. Ein Repository namens ".profile" or ".profile-private" kann verwendet werden, um die README.md auf dem Benutzer- oder Organisationsprofil anzuzeigen.
repo_size=Repository-Größe
template=Template
template_select=Vorlage auswählen
@@ -1116,6 +1133,7 @@ blame.ignore_revs=Revisionen in .git-blame-ignore-revs werden i
blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs.
user_search_tooltip=Zeigt maximal 30 Benutzer
+tree_path_not_found=Pfad %[1]s existiert nicht in %[2]s
transfer.accept=Übertragung Akzeptieren
transfer.accept_desc=`Übertragung nach "%s"`
@@ -1126,6 +1144,7 @@ transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abz
desc.private=Privat
desc.public=Öffentlich
+desc.public_access=Öffentlicher Zugriff
desc.template=Template
desc.internal=Intern
desc.archived=Archiviert
@@ -1209,6 +1228,7 @@ migrate.migrating_issues=Issues werden migriert
migrate.migrating_pulls=Pull Requests werden migriert
migrate.cancel_migrating_title=Migration abbrechen
migrate.cancel_migrating_confirm=Möchtest du diese Migration abbrechen?
+migrating_status=Migrationstatus
mirror_from=Mirror von
forked_from=geforkt von
@@ -1233,6 +1253,7 @@ create_new_repo_command=Erstelle ein neues Repository von der Kommandozeile aus
push_exist_repo=Bestehendes Repository via Kommandozeile pushen
empty_message=Dieses Repository hat keinen Inhalt.
broken_message=Die Git-Daten, die diesem Repository zugrunde liegen, können nicht gelesen werden. Kontaktiere den Administrator deiner Instanz oder lösche dieses Repository.
+no_branch=Dieses Repository hat keine Branches.
code=Code
code.desc=Zugriff auf Quellcode, Dateien, Commits und Branches.
@@ -1344,6 +1365,8 @@ editor.new_branch_name_desc=Neuer Branchname…
editor.cancel=Abbrechen
editor.filename_cannot_be_empty=Der Dateiname darf nicht leer sein.
editor.filename_is_invalid=Ungültiger Dateiname: "%s".
+editor.commit_email=Commit-E-Mail-Adresse
+editor.invalid_commit_email=Die E-Mail-Adresse für den Commit ist ungültig.
editor.branch_does_not_exist=Branch "%s" existiert nicht in diesem Repository.
editor.branch_already_exists=Branch "%s" existiert bereits in diesem Repository.
editor.directory_is_a_file=Der Verzeichnisname "%s" wird bereits als Dateiname in diesem Repository verwendet.
@@ -1392,6 +1415,7 @@ commits.signed_by_untrusted_user_unmatched=Signiert von nicht vertrauenswürdige
commits.gpg_key_id=GPG-Schlüssel-ID
commits.ssh_key_fingerprint=SSH-Key-Fingerabdruck
commits.view_path=An diesem Punkt im Verlauf anzeigen
+commits.view_file_diff=Änderungen an dieser Datei in diesem Commit anzeigen
commit.operations=Operationen
commit.revert=Zurücksetzen
@@ -1452,6 +1476,8 @@ issues.filter_milestones=Meilenstein filtern
issues.filter_projects=Projekt filtern
issues.filter_labels=Label filtern
issues.filter_reviewers=Reviewer filtern
+issues.filter_no_results=Keine Ergebnisse
+issues.filter_no_results_placeholder=Versuche, deine Suchfilter anzupassen.
issues.new=Neues Issue
issues.new.title_empty=Der Titel kann nicht leer sein
issues.new.labels=Label
@@ -1527,11 +1553,14 @@ issues.filter_project=Projekt
issues.filter_project_all=Alle Projekte
issues.filter_project_none=Kein Projekt
issues.filter_assignee=Zuständig
+issues.filter_assignee_no_assignee=Niemandem zugewiesen
+issues.filter_assignee_any_assignee=Jemandem zugewiesen
issues.filter_poster=Autor
issues.filter_user_placeholder=Benutzer suchen
issues.filter_user_no_select=Alle Benutzer
issues.filter_type=Typ
issues.filter_type.all_issues=Alle Issues
+issues.filter_type.all_pull_requests=Alle Pull-Requests
issues.filter_type.assigned_to_you=Dir zugewiesen
issues.filter_type.created_by_you=Von dir erstellt
issues.filter_type.mentioning_you=Hat dich erwähnt
@@ -1623,12 +1652,15 @@ issues.save=Speichern
issues.label_title=Labelname
issues.label_description=Labelbeschreibung
issues.label_color=Labelfarbe
+issues.label_color_invalid=Ungültige Farbe
issues.label_exclusive=Exklusiv
issues.label_archive=Label archivieren
issues.label_archived_filter=Archivierte Labels anzeigen
issues.label_archive_tooltip=Archivierte Labels werden bei der Suche nach Label standardmäßig von den Vorschlägen ausgeschlossen.
issues.label_exclusive_desc=Nenne das Label Bereich/Element um es gegenseitig ausschließend mit anderen Bereich/ Labels zu machen.
issues.label_exclusive_warning=Alle im Konflikt stehenden Labels werden beim Bearbeiten der Labels eines Issues oder eines Pull-Requests entfernt.
+issues.label_exclusive_order=Sortierreihenfolge
+issues.label_exclusive_order_tooltip=Exklusive Labels im gleichen Bereich werden nach dieser numerischen Reihenfolge sortiert.
issues.label_count=%d Label
issues.label_open_issues=%d offene Issues
issues.label_edit=Bearbeiten
@@ -1680,14 +1712,18 @@ issues.timetracker_timer_manually_add=Zeit hinzufügen
issues.time_estimate_set=Geschätzte Zeit festlegen
issues.time_estimate_display=Schätzung: %s
+issues.change_time_estimate_at=Zeitschätzung geändert zu %[1]s %[2]s
issues.remove_time_estimate_at=Zeitschätzung %s entfernt
issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig
issues.start_tracking_history=hat die Zeiterfassung %s gestartet
issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird
issues.tracking_already_started=`Du hast die Zeiterfassung bereits in diesem Issue gestartet!`
+issues.stop_tracking=Timer stoppen
+issues.stop_tracking_history=hat für %[1]s %[2]s gearbeitet
issues.cancel_tracking=Verwerfen
issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen`
issues.del_time=Diese Zeiterfassung löschen
+issues.add_time_history=hat %[1]s %[2]s gearbeitete Zeit hinzugefügt
issues.del_time_history=`hat %s gearbeitete Zeit gelöscht`
issues.add_time_manually=Zeit manuell hinzufügen
issues.add_time_hours=Stunden
@@ -1847,6 +1883,7 @@ pulls.add_prefix=%s Präfix hinzufügen
pulls.remove_prefix=%s Präfix entfernen
pulls.data_broken=Dieser Pull-Requests ist kaputt, da Fork-Informationen gelöscht wurden.
pulls.files_conflicted=Dieser Pull-Request hat Änderungen, die im Widerspruch zum Ziel-Branch stehen.
+pulls.is_checking=Die Konfliktprüfung läuft noch. Bitte aktualisiere die Seite in wenigen Augenblicken.
pulls.is_ancestor=Dieser Branch ist bereits im Zielbranch enthalten. Es gibt nichts zu mergen.
pulls.is_empty=Die Änderungen an diesem Branch sind bereits auf dem Zielbranch. Dies wird ein leerer Commit sein.
pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich.
@@ -1916,6 +1953,7 @@ pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits
pulls.close=Pull-Request schließen
pulls.closed_at=`hat diesen Pull-Request %[2]s geschlossen`
pulls.reopened_at=`hat diesen Pull-Request %[2]s wieder geöffnet`
+pulls.cmd_instruction_hint=Zeige Kommandozeilenanweisungen
pulls.cmd_instruction_checkout_title=Checkout
pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
pulls.cmd_instruction_merge_title=Mergen
@@ -1944,6 +1982,7 @@ pulls.upstream_diverging_prompt_behind_1=Dieser Branch ist %[1]d Commit hinter %
pulls.upstream_diverging_prompt_behind_n=Dieser Branch ist %[1]d Commits hinter %[2]s
pulls.upstream_diverging_prompt_base_newer=Der Basis-Branch %s hat neue Änderungen
pulls.upstream_diverging_merge=Fork synchronisieren
+pulls.upstream_diverging_merge_confirm=Möchtest du „%[1]s“ in „%[2]s“ mergen?
pull.deleted_branch=(gelöscht):%s
pull.agit_documentation=Dokumentation zu AGit durchschauen
@@ -2103,6 +2142,12 @@ contributors.contribution_type.deletions=Löschungen
settings=Einstellungen
settings.desc=In den Einstellungen kannst du die Einstellungen des Repositories anpassen
settings.options=Repository
+settings.public_access=Öffentlicher Zugriff
+settings.public_access_desc=Konfiguriere die Zugriffsrechte öffentlicher Besucher, um die Standardwerte dieses Repositorys zu überschreiben.
+settings.public_access.docs.not_set=Nicht gesetzt: keine zusätzliche öffentliche Zugriffsberechtigung. Die Berechtigung der Besucher folgt den Sichtbarkeits- und Mitgliedsberechtigungen des Repositorys.
+settings.public_access.docs.anonymous_read=Anonymes Lesen: Nicht angemeldete Benutzer können mit Leseberechtigung auf die Einheit zugreifen.
+settings.public_access.docs.everyone_read=Alle Lesen: Alle angemeldeten Benutzer können mit Leseberechtigung auf die Einheit zugreifen. Leseberechtigung für Issues/Pull-Request-Einheiten bedeutet auch, dass Benutzer neue Issues/Pull-Requests erstellen können.
+settings.public_access.docs.everyone_write=Alle Schreiben: Alle angemeldeten Benutzer haben Schreibrechte auf die Einheit. Nur die Wiki-Einheit unterstützt diese Berechtigung.
settings.collaboration=Mitarbeiter
settings.collaboration.admin=Administrator
settings.collaboration.write=Schreibrechte
@@ -2316,6 +2361,8 @@ settings.event_fork=Fork
settings.event_fork_desc=Repository geforkt.
settings.event_wiki=Wiki
settings.event_wiki_desc=Wiki-Seite erstellt, umbenannt, bearbeitet oder gelöscht.
+settings.event_statuses=Status
+settings.event_statuses_desc=Commit-Status von der API aktualisiert.
settings.event_release=Release
settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht.
settings.event_push=Push
@@ -2353,6 +2400,9 @@ settings.event_pull_request_review_request=Überprüfung des Pull-Requests angef
settings.event_pull_request_review_request_desc=Überprüfung des Pull-Requests angefragt oder die Anfrage entfernt.
settings.event_pull_request_approvals=Zustimmungen zum Pull-Request
settings.event_pull_request_merge=Pull-Request-Merge
+settings.event_header_workflow=Workflow-Ereignisse
+settings.event_workflow_job=Workflow-Jobs
+settings.event_workflow_job_desc=Gitea Actions Workflow Job in Warteschlange, wartend, in Bearbeitung oder abgeschlossen.
settings.event_package=Paket
settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht.
settings.branch_filter=Branch-Filter
@@ -2615,6 +2665,9 @@ diff.image.overlay=Überlagert
diff.has_escaped=Diese Zeile enthält versteckte Unicode-Zeichen
diff.show_file_tree=Dateibaum anzeigen
diff.hide_file_tree=Dateibaum ausblenden
+diff.submodule_added=Submodul %[1]s hinzugefügt bei %[2]s
+diff.submodule_deleted=Submodul %[1]s gelöscht von %[2]s
+diff.submodule_updated=Submodul %[1]s aktualisiert: %[2]s
releases.desc=Behalte den Überblick über Versionen und Downloads.
release.releases=Releases
@@ -2685,6 +2738,7 @@ branch.restore_success=Branch "%s" wurde wiederhergestellt.
branch.restore_failed=Wiederherstellung vom Branch "%s" gescheitert.
branch.protected_deletion_failed=Branch "%s" ist geschützt und kann nicht gelöscht werden.
branch.default_deletion_failed=Branch "%s" kann nicht gelöscht werden, da dieser Branch der Standard-Branch ist.
+branch.default_branch_not_exist=Standardzweig „%s“ existiert nicht.
branch.restore=Branch "%s" wiederherstellen
branch.download=Branch "%s" herunterladen
branch.rename=Branch "%s" umbenennen
@@ -2699,6 +2753,8 @@ branch.create_branch_operation=Branch erstellen
branch.new_branch=Neue Branch erstellen
branch.new_branch_from=Neuen Branch von "%s" erstellen
branch.renamed=Branch %s wurde in %s umbenannt.
+branch.rename_default_or_protected_branch_error=Nur Administratoren können Standard- oder geschützte Branches umbenennen.
+branch.rename_protected_branch_failed=Dieser Branch ist durch globale Schutzregeln geschützt.
tag.create_tag=Tag %s erstellen
tag.create_tag_operation=Tag erstellen
@@ -2853,7 +2909,11 @@ teams.invite.title=Du wurdest eingeladen, dem Team %s in der Or
teams.invite.by=Von %s eingeladen
teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten.
+view_as_role=Ansehen als: %s
+view_as_public_hint=Du siehst die README als ein öffentlicher Benutzer.
+view_as_member_hint=Du siehst die README als ein Mitglied dieser Organisation.
+worktime=Arbeitszeit
worktime.date_range_start=Startdatum
worktime.date_range_end=Enddatum
worktime.query=Abfrage
@@ -3354,6 +3414,8 @@ monitor.previous=Letzte Ausführung
monitor.execute_times=Ausführungen
monitor.process=Laufende Prozesse
monitor.stacktrace=Stacktraces
+monitor.trace=Ablaufverfolgung
+monitor.performance_logs=Leistungsprotokolle
monitor.processes_count=%d Prozesse
monitor.download_diagnosis_report=Diagnosebericht herunterladen
monitor.desc=Beschreibung
@@ -3528,6 +3590,7 @@ versions=Versionen
versions.view_all=Alle anzeigen
dependency.id=ID
dependency.version=Version
+search_in_external_registry=In %s suchen
alpine.registry=Richte diese Registry ein, indem Du die URL in die /etc/apk/repositories-Datei hinzufügst:
alpine.registry.key=Lade den öffentlichen RSA-Key der Registry in den /etc/apk/keys/-Ordner, um die Signatur zu überprüfen:
alpine.registry.info=Wähle $branch und $repository aus der Liste unten.
@@ -3557,6 +3620,8 @@ conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befeh
container.details.type=Container-Image Typ
container.details.platform=Plattform
container.pull=Downloade das Container-Image aus der Kommandozeile:
+container.images=Images
+container.digest=Digest
container.multi_arch=Betriebsystem / Architektur
container.layers=Container-Image Ebenen
container.labels=Labels
@@ -3664,8 +3729,13 @@ none=Noch keine Secrets vorhanden.
creation.description=Beschreibung
creation.name_placeholder=Groß-/Kleinschreibung wird ignoriert, nur alphanumerische Zeichen oder Unterstriche, darf nicht mit GITEA_ oder GITHUB_ beginnen
creation.value_placeholder=Beliebigen Inhalt eingeben. Leerzeichen am Anfang und Ende werden weggelassen.
+creation.description_placeholder=Gib eine Kurzbeschreibung ein (optional).
+save_success=Das Secret "%s" wurde gespeichert.
+save_failed=Secret konnte nicht gespeichert werden.
+add_secret=Secret hinzufügen
+edit_secret=Secret bearbeiten
deletion=Secret entfernen
deletion.description=Das Entfernen eines Secrets kann nicht rückgängig gemacht werden. Fortfahren?
deletion.success=Das Secret wurde entfernt.
@@ -3743,6 +3813,10 @@ runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest d
runs.no_runs=Der Workflow hat noch keine Ausführungen.
runs.empty_commit_message=(leere Commit-Nachricht)
runs.expire_log_message=Protokolle wurden geleert, weil sie zu alt waren.
+runs.delete=Workflow-Ausführung löschen
+runs.delete.description=Bist du sicher, dass du diese Workflow-Ausführung dauerhaft löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
+runs.not_done=Diese Workflow-Ausführung ist noch nicht abgeschlossen.
+runs.view_workflow_file=Workflow-Datei anzeigen
workflow.disable=Workflow deaktivieren
workflow.disable_success=Workflow '%s' erfolgreich deaktiviert.
@@ -3754,6 +3828,7 @@ workflow.not_found=Workflow '%s' wurde nicht gefunden.
workflow.run_success=Workflow '%s' erfolgreich ausgeführt.
workflow.from_ref=Nutze Workflow von
workflow.has_workflow_dispatch=Dieser Workflow hat einen workflow_dispatch Event-Trigger.
+workflow.has_no_workflow_dispatch=Der Workflow '%s' hat keinen workflow_dispatch Event-Trigger.
need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich.
@@ -3781,6 +3856,8 @@ deleted.display_name=Gelöschtes Projekt
type-1.display_name=Individuelles Projekt
type-2.display_name=Repository-Projekt
type-3.display_name=Organisationsprojekt
+enter_fullscreen=Vollbild
+exit_fullscreen=Vollbild verlassen
[git.filemode]
changed_filemode=%[1]s → %[2]s
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 6e4528b056..42df483d8d 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -4,6 +4,7 @@ explore=Огляд
help=Довідка
logo=Логотип
sign_in=Увійти
+sign_in_with_provider=Увійдіть за допомогою %s
sign_in_or=або
sign_out=Вийти
sign_up=Реєстрація
@@ -29,14 +30,25 @@ more_items=Більше елементів
username=Ім'я кристувача
email=Адреса електронної пошти
password=Пароль
-access_token=Токен Доступу
+access_token=Токен доступу
re_type=Підтвердження пароля
captcha=CAPTCHA
-twofa=Двофакторна авторизація
+twofa=Двофакторна автентифікація
twofa_scratch=Двофакторний одноразовий пароль
passcode=Код доступу
webauthn_insert_key=Вставте ключ безпеки
+webauthn_sign_in=Натисніть кнопку на вашому ключі безпеки. Якщо ваш ключ без фізичної кнопки, поновно вставте ключ.
+webauthn_press_button=Будь ласка, натисніть кнопку на вашому ключі безпеки…
+webauthn_use_twofa=Використовуйте дво-факторний код із Вашого телефона
+webauthn_error=Не вдалося прочитати ваш ключ безпеки.
+webauthn_unsupported_browser=Ваш браузер наразі не підтримує WebAuthn.
+webauthn_error_unknown=Сталася невідома помилка. Будь ласка, спробуйте ще раз.
+webauthn_error_insecure=`WebAuthn підтримує тільки безпечні з’єднання. Для тестування по HTTP, ви можете скористатися "localhost" або "127.0.0.1"`
+webauthn_error_unable_to_process=Сервер не зміг обробити ваш запит.
+webauthn_error_duplicated=Ключ безпеки не підходить для цього запиту. Переконайтеся, що ключ ще не зареєстровано.
+webauthn_error_empty=Ви повинні встановити назву для цього ключа.
+webauthn_error_timeout=Час очікування вичерпано, перш ніж ваш ключ було прочитано. Перезавантажте сторінку та спробуйте ще раз.
webauthn_reload=Оновити
repository=Репозиторій
@@ -46,7 +58,6 @@ issue_milestone=Етап
new_repo=Новий репозиторій
new_migrate=Нова міграція
new_mirror=Нове дзеркало
-new_fork=Новий репозиторій - копія
new_org=Нова організація
new_project=Новий проєкт
new_project_column=Новий стовпець
@@ -59,7 +70,7 @@ your_starred=Обрані
your_settings=Налаштування
all=Усі
-sources=Власні
+sources=Джерела
mirrors=Дзеркала
collaborative=Спільні
forks=Форки
@@ -71,6 +82,7 @@ milestones=Етапи
ok=OK
cancel=Відмінити
+retry=Повторіть спробу
rerun=Перезапустити
rerun_all=Перезапустити всі завдання
save=Зберегти
@@ -102,12 +114,13 @@ loading=Завантаження…
files=Файли
error=Помилка
-error404=Сторінка, до якої ви намагаєтеся звернутися або до , не існує або Ви не маєте права на її перегляд.
+error404=Сторінка, яку ви намагаєтеся відкрити, не існує або ви не маєте права на її перегляд.
error503=Сервер не зміг виконати ваш запит. Будь ласка, спробуйте пізніше.
go_back=Назад
invalid_data=Недійсні дані: %v
never=Ніколи
+unknown=Невідомо
rss_feed=Стрічка RSS
@@ -126,6 +139,7 @@ concept_user_organization=Організація
show_timestamps=Показувати часові мітки
show_log_seconds=Показувати секунди
show_full_screen=Показати на весь екран
+download_logs=Завантажити журнали
confirm_delete_selected=Підтверджуєте видалення всіх вибраних елементів?
@@ -135,14 +149,35 @@ value=Значення
filter=Фільтр
filter.clear=Очистити фільтр
filter.is_archived=Архівовані
+filter.not_archived=Не архівовано
+filter.is_fork=Відгалужено
+filter.not_fork=Не відгалужено
+filter.is_mirror=Віддзеркалено
+filter.not_mirror=Не віддзеркалено
filter.is_template=Шаблон
+filter.not_template=Не шаблон
filter.public=Публічний
filter.private=Приватний
+no_results_found=Нічого не знайдено.
[search]
+search=Пошук...
+type_tooltip=Тип пошуку
+fuzzy=Неточний
+regexp=Регулярний вираз
+user_kind=Пошук користувачів...
+org_kind=Пошук організацій...
+team_kind=Пошук команд...
+code_kind=Пошук коду...
+code_search_unavailable=Пошук коду наразі недоступний. Будь ласка, зверніться до адміністратора сайту.
+code_search_by_git_grep=Поточні результати пошуку коду надаються командою "git grep". Результати можуть бути кращими, якщо адміністратор сайту увімкне індексатор сховища.
+project_kind=Пошук проєктів...
+branch_kind=Пошук гілок...
+commit_kind=Пошук комітів...
no_results=Не знайдено жодного збігу.
issue_kind=Пошук задач...
+pull_kind=Пошук запитів на злиття...
keyword_search_unavailable=Пошук за ключовими словами наразі недоступний. Будь ласка, зверніться до адміністратора сайту.
[aria]
@@ -158,6 +193,7 @@ more=Більше
[editor]
buttons.heading.tooltip=Додати заголовок
+buttons.bold.tooltip=Додати грубий текст
buttons.italic.tooltip=Додати курсивний текст
buttons.quote.tooltip=Цитувати текст
buttons.code.tooltip=Додати код
@@ -303,6 +339,7 @@ env_config_keys_prompt=Наступні змінні середовища так
config_write_file_prompt=Ці параметри будуть записані в: %s
[home]
+nav_menu=Меню навігації
uname_holder=Ім'я користувача або Ел. пошта
password_holder=Пароль
switch_dashboard_context=Переключити контекст панелі управління
@@ -328,6 +365,7 @@ show_only_public=Показано тільки публічні
issues.in_your_repos=В ваших репозиторіях
+guide_title=Жодної активності
[explore]
repos=Репозиторії
@@ -392,6 +430,7 @@ password_pwned_err=Не вдалося виконати запит до HaveIBee
[mail]
view_it_on=Переглянути на %s
+reply=або надішліть відповідь безпосередньо на цей електронний лист
link_not_working_do_paste=Не працює? Спробуйте скопіювати та вставити його в свій браузер.
hi_user_x=Привіт %s,
@@ -401,6 +440,7 @@ activate_account.text_1=Привіт, %[1]s, дякуємо за реєс
activate_account.text_2=Перейдіть за цим посиланням, щоб активувати ваш обліковий запис в %s:
activate_email=Підтвердить вашу адресу електронної пошти
+activate_email.title=%s, будь ласка, підтвердіть вашу адресу електронної пошти
activate_email.text=Перейдіть за цим посиланням, щоб підтвердити вашу електронну адресу в %s:
register_notify.title=%[1]s, ласкаво просимо до %[2]s
@@ -449,6 +489,8 @@ repo.collaborator.added.subject=%s додав вас до %s
repo.collaborator.added.text=Ви були додані в якості співавтора репозиторію:
team_invite.subject=%[1]s запрошує вас приєднатися до організації %[2]s
+team_invite.text_1=%[1]s запрошує вас до команди %[2]s в організації %[3]s.
+team_invite.text_2=Перейдіть за посиланням, щоб приєднатися до команди:
[modal]
yes=Так
@@ -536,6 +578,7 @@ auth_failed=Помилка автентифікації: %v
target_branch_not_exist=Цільової гілки не існує.
+target_ref_not_exist=Цільове посилання не існує %s
admin_cannot_delete_self=Ви не можете видалити себе, допоки ви адміністратор. Будь ласка, спочатку видаліть права адміністратора.
@@ -572,6 +615,10 @@ block.title=Заблокувати користувача
block.info=Блокування користувача не дозволяє йому взаємодіяти зі сховищами, наприклад, відкривати або коментувати запити на злиття або задачі. Дізнайтеся більше про блокування користувача.
block.info_2=слідкують за вашим обліковим записом
block.info_3=надсилати вам сповіщення @згадавши ваше ім'я
+block.note=Примітка
+block.note.title=Необов’язкова примітка:
+block.list=Заблоковані користувачі
+block.list.none=Ви не заблокували жодного користувача.
[settings]
profile=Профіль
@@ -586,6 +633,7 @@ applications=Додатки
orgs=Керування організаціями
repos=Репозиторії
delete=Видалити обліковий запис
+twofa=Двофакторна автентифікація (TOTP)
account_link=Прив'язані облікові записи
organization=Організації
@@ -603,6 +651,7 @@ continue=Продовжити
cancel=Відмінити
language=Мова
ui=Тема
+hidden_comment_types=Приховані типи коментарів
comment_type_group_reference=Посилання
comment_type_group_label=Мітка
comment_type_group_milestone=Етап
@@ -763,6 +812,7 @@ permission_write=Читання і запис
permission_anonymous_read=Анонімне читання
permission_everyone_read=Усі читають
permission_everyone_write=Усі пишуть
+permissions_list=Дозволи:
manage_oauth2_applications=Керування програмами OAuth2
edit_oauth2_application=Редагувати програму OAuth2
@@ -824,6 +874,7 @@ email_notifications.enable=Увімкнути сповіщення email
email_notifications.onmention=Повідомлення email тільки коли згадують
email_notifications.disable=Вимкнути email сповіщення
email_notifications.submit=Налаштувати параметри email
+email_notifications.andyourown=І ваші власні повідомлення
visibility=Видимість користувача
visibility.public=Публічний
@@ -995,6 +1046,9 @@ migrate.migrating_labels=Міграція міток
migrate.migrating_releases=Міграція релізів
migrate.migrating_issues=Міграція задач
migrate.migrating_pulls=Міграція запитів на злиття
+migrate.cancel_migrating_title=Скасувати міграцію
+migrate.cancel_migrating_confirm=Ви хочете скасувати міграцію?
+migrating_status=Cтатус міграції
mirror_from=дзеркало
forked_from=форк від
@@ -1008,10 +1062,12 @@ watch=Слідкувати
unstar=Видалити із обраних
star=В обрані
fork=Форк
+action.blocked_user=Неможливо виконати дію, оскільки ви заблоковані власником сховища.
download_archive=Скачати репозиторій
quick_guide=Короткий посібник
clone_this_repo=Кнонувати цей репозиторій
+cite_this_repo=Послатися на це сховище
create_new_repo_command=Створити новий репозиторій з командного рядка
push_exist_repo=Опублікувати існуючий репозиторій з командного рядка
empty_message=Цей репозиторій порожній.
@@ -1028,6 +1084,7 @@ tags=Теги
issues=Задачі
pulls=Запити на злиття
projects=Проєкти
+actions=Дії
labels=Мітки
org_labels_desc=Мітки рівня організації можуть використовуватися в усіх репозиторіях цієї організації
org_labels_desc_manage=керувати
@@ -1047,6 +1104,12 @@ file_view_rendered=Переглянути відрендерено
file_view_raw=Перегляд Raw
file_permalink=Постійне посилання
file_too_large=Цей файл завеликий щоб бути показаним.
+file_is_empty=Файл порожній.
+code_preview_line_from_to=Рядки від %[1]d до %[2]d в %[3]s
+code_preview_line_in=Рядок %[1]d в %[2]s
+invisible_runes_header=`Цей файл містить невидимі символи Юнікоду`
+ambiguous_runes_header=`Цей файл містить неоднозначні символи Юнікоду`
+invisible_runes_line=`Цей рядок містить невидимі символи Юнікоду`
escape_control_characters=Екранувати
unescape_control_characters=Відмінити екранування
@@ -1102,11 +1165,15 @@ editor.propose_file_change=Запропонувати зміну файлу
editor.new_branch_name_desc=Ім'я нової гілки…
editor.cancel=Відмінити
editor.filename_cannot_be_empty=Ім'я файлу не може бути порожнім.
+editor.invalid_commit_email=Адреса електронної пошти для коміту недійсна.
editor.file_changed_while_editing=Зміст файлу змінився з моменту початку редагування. Натисніть тут , щоб переглянути що було змінено, або закомітьте зміни ще раз, щоб переписати їх.
editor.commit_empty_file_header=Закомітити порожній файл
editor.commit_empty_file_text=Файл, в комміті порожній. Продовжити?
editor.no_changes_to_show=Нема змін для показу.
+editor.fail_to_update_file=Не вдалося оновити/створити файл "%s".
editor.fail_to_update_file_summary=Помилка:
+editor.push_rejected_no_message=Зміну відхилено сервером без повідомлення. Будь ласка, перевірте Git-хуки.
+editor.push_rejected=Зміну відхилено сервером. Будь ласка, перевірте Git-хуки.
editor.push_rejected_summary=Повне повідомлення про відмову:
editor.add_subdir=Додати каталог…
editor.upload_file_is_locked=Файл "%s" заблоковано %s.
@@ -1130,10 +1197,14 @@ commits.signed_by_untrusted_user=Підписаний недовіреним к
commits.signed_by_untrusted_user_unmatched=Підписаний недовіреним користувачем, який не відповідає комітеру
commits.gpg_key_id=Ідентифікатор GPG ключа
commits.ssh_key_fingerprint=Відбиток ключа SSH
+commits.view_file_diff=Переглянути зміни до цього файлу в цьому коміті
+commit.revert-content=Виберіть гілку, до якої хочете повернутися:
commitstatus.error=Помилка
+commitstatus.failure=Невдача
commitstatus.pending=Очікування
+commitstatus.success=Успіх
ext_issues=Доступ до зовнішніх задач
ext_issues.desc=Посилання на зовнішню систему відстеження задач.
@@ -1144,19 +1215,23 @@ projects.create=Створити проєкт
projects.title=Назва
projects.new=Новий проєкт
projects.new_subheader=Координуйте, відстежуйте та оновлюйте інформацію про виконувану роботу в одному місці, аби проєкти залишалися прозорими та за розкладом.
+projects.create_success=Проєкт "%s" створено.
projects.deletion=Видалити проєкт
projects.deletion_desc=Видалення проєкту видаляє його з усіх пов'язаних задач. Продовжити?
projects.deletion_success=Проєкт видалено.
projects.edit=Редагувати проєкти
projects.edit_subheader=Проєкти організовують задачі та відстежують прогрес.
projects.modify=Оновити проєкт
+projects.edit_success=Проєкт "%s" оновлено.
projects.type.none=Відсутній
projects.type.basic_kanban=Спрощений канбан
projects.type.bug_triage=Сортування помилок
projects.template.desc=Шаблон проєкту
projects.template.desc_helper=Оберіть шаблон проєкту, аби почати
+projects.column.edit=Редагувати стовпець
projects.column.edit_title=Назва
projects.column.new_title=Назва
+projects.column.new_submit=Створити стовпець
projects.column.new=Новий стовпець
projects.column.set_default=Встановити типово
projects.column.delete=Видалити стовпець
@@ -1197,6 +1272,9 @@ issues.choose.get_started=Початок роботи
issues.choose.open_external_link=Відкрити
issues.choose.blank=Типово
issues.choose.blank_about=Створити задачу із шаблону за замовчуванням.
+issues.choose.ignore_invalid_templates=Недійсні шаблони проігноровано
+issues.choose.invalid_templates=Знайдено %v недійсний(х) шаблон(ів)
+issues.choose.invalid_config=Конфігурація задачі містить помилки:
issues.no_ref=Не вказана гілка або тег
issues.create=Створити задачу
issues.new_label=Нова мітка
@@ -1250,7 +1328,6 @@ issues.filter_type.all_pull_requests=Усі запити на злиття
issues.filter_type.assigned_to_you=Призначене вам
issues.filter_type.created_by_you=Створено вами
issues.filter_type.mentioning_you=Вас згадано
-issues.filter_type.review_requested=Відгук запитано
issues.filter_type.reviewed_by_you=Перевірено вами
issues.filter_sort=Сортувати
issues.filter_sort.latest=Найновіші
@@ -1317,6 +1394,7 @@ issues.save=Зберегти
issues.label_title=Назва мітки
issues.label_description=Опис мітки
issues.label_color=Колір мітки
+issues.label_exclusive_order=Порядок сортування
issues.label_count=%d міток
issues.label_open_issues=%d відкритих задач
issues.label_edit=Редагувати
@@ -1335,6 +1413,7 @@ issues.attachment.download=`Натисніть щоб завантажити "%s
issues.subscribe=Підписатися
issues.unsubscribe=Відписатися
issues.unpin=Відкріпити
+issues.max_pinned=Ви не можете прикріпити більше задач
issues.lock=Блокування обговорення
issues.unlock=Розблокування обговорення
issues.lock_duplicate=Задача не може бути заблокованим двічі.
@@ -1356,6 +1435,7 @@ issues.comment_on_locked=Ви не можете коментувати забл
issues.delete=Видалити
issues.tracker=Відстеження часу
+issues.timetracker_timer_start=Запустити таймер
issues.timetracker_timer_stop=Зупинити таймер
issues.timetracker_timer_discard=Скинути таймер
issues.timetracker_timer_manually_add=Додати час
@@ -1363,8 +1443,10 @@ issues.timetracker_timer_manually_add=Додати час
issues.tracker_auto_close=Таймер буде автоматично зупинено, коли ця задача буде закрита
issues.tracking_already_started=`Ви вже почали відстежувати час для іншої задачі!`
issues.stop_tracking=Зупинити таймер
+issues.cancel_tracking=Скинути
issues.del_time=Видалити цей журнал часу
issues.del_time_history=`видалив витрачений час %s`
+issues.add_time_manually=Вручну додати час
issues.add_time_hours=Години
issues.add_time_minutes=Хвилини
issues.add_time_sum_to_small=Час не введено.
@@ -1389,6 +1471,11 @@ issues.due_date_remove=видалив(ла) дату завершення %s %s
issues.due_date_overdue=Прострочено
issues.due_date_invalid=Термін дії не дійсний або знаходиться за межами допустимого діапазону. Будь ласка використовуйте формат 'yyyy-mm-dd'.
issues.dependency.title=Залежності
+issues.dependency.issue_no_dependencies=Залежностей не встановлено.
+issues.dependency.pr_no_dependencies=Залежностей не встановлено.
+issues.dependency.no_permission_1=У вас немає дозволу на читання %d залежності
+issues.dependency.no_permission_n=Ви не маєте дозволу на читання %d залежностей
+issues.dependency.no_permission.can_remove=Ви не маєте дозволу на читання цієї залежності, але можете видалити її.
issues.dependency.add=Додати залежність…
issues.dependency.cancel=Відмінити
issues.dependency.remove=Видалити
@@ -1416,6 +1503,7 @@ issues.dependency.add_error_dep_not_same_repo=Обидві задачі пови
issues.review.self.approval=Ви не можете схвалити власний пулл-реквест.
issues.review.self.rejection=Ви не можете надіслати запит на зміну на власний пулл-реквест.
issues.review.approve=зміни затверджено %s
+issues.review.comment=рецензовано %s
issues.review.dismissed=відхилено відгук %s %s
issues.review.dismissed_label=Відхилено
issues.review.left_comment=додав коментар
@@ -1426,9 +1514,13 @@ issues.review.add_review_request=попросив рецензію від %s %s
issues.review.remove_review_request=видалив запит на рецензію до %s %s
issues.review.remove_review_request_self=відмовився рецензувати %s
issues.review.pending=Очікування
+issues.review.pending.tooltip=Цей коментар наразі не видно іншим користувачам. Щоб відправити свій коментар, виберіть "%s" -> "%s/%s/%s" у верхній частині сторінки.
issues.review.review=Рецензії
issues.review.reviewers=Рецензенти
issues.review.outdated=Застарілі
+issues.review.outdated_description=Вміст змінився з моменту створення цього коментаря
+issues.review.option.show_outdated_comments=Показати застарілі коментарі
+issues.review.option.hide_outdated_comments=Приховати застарілі коментарі
issues.review.show_outdated=Показати застарілі
issues.review.hide_outdated=Приховати застарілі
issues.review.show_resolved=Показати вирішене
@@ -1437,6 +1529,7 @@ issues.review.resolve_conversation=Завершити обговорення
issues.review.un_resolve_conversation=Поновити обговорення
issues.review.resolved_by=позначив обговорення завершеним
issues.review.commented=Коментар
+issues.review.official=Затверджено
issues.assignee.error=Додано не всіх виконавців через непередбачену помилку.
issues.reference_issue.body=Тіло
issues.content_history.deleted=видалено
@@ -1445,19 +1538,25 @@ issues.content_history.created=створено
issues.content_history.delete_from_history=Видалити з історії
issues.content_history.delete_from_history_confirm=Видалити з історії?
issues.content_history.options=Налаштування
+issues.reference_link=Посилання: %s
compare.compare_base=основа
compare.compare_head=порівняти
pulls.desc=Увімкнути запити на злиття та огляд коду.
pulls.new=Новий запит на злиття
+pulls.view=Переглянути запит на злиття
pulls.compare_changes=Новий запит на злиття
pulls.compare_changes_desc=Порівняти дві гілки і створити запит на злиття для змін.
+pulls.has_viewed_file=Переглядів
+pulls.expand_files=Розгорнути всі файли
+pulls.collapse_files=Згорнути всі файли
pulls.compare_base=злити в
pulls.compare_compare=pull з
pulls.switch_comparison_type=Перемкнути вигляд порівняння
pulls.switch_head_and_base=Поміняти місцями основну та базову гілку
pulls.filter_branch=Фільтр по гілці
+pulls.show_all_commits=Показати всі коміти
pulls.nothing_to_compare=Ці гілки однакові. Немає необхідності створювати запитів на злиття.
pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім.
pulls.has_pull_request=`Запит злиття для цих гілок вже існує: %[2]s#%[3]d`
@@ -1471,7 +1570,9 @@ pulls.tab_files=Змінені файли
pulls.reopen_to_merge=Будь ласка перевідкрийте цей запит щоб здіснити операцію злиття.
pulls.cant_reopen_deleted_branch=Цей запит не можна повторно відкрити, оскільки гілку видалено.
pulls.merged=Злито
+pulls.closed=Запит на злиття закрито
pulls.manually_merged=Ручне злиття
+pulls.merged_info_text=Гілку %s тепер можна видалити.
pulls.is_closed=Запит на злиття було закрито.
pulls.title_wip_desc=`Почніть заголовок з %s щоб запобігти випадковому злиттю запитів.`
pulls.cannot_merge_work_in_progress=Цей пулл-реквест позначений як прийнятий в опрацювання.
@@ -1480,6 +1581,7 @@ pulls.add_prefix=Додати префікс %s
pulls.remove_prefix=Видалити префікс %s
pulls.data_broken=Зміст цього запиту було порушено внаслідок видалення інформації Форком. Цей запит тягнеться через відсутність інформації про вилучення.
pulls.files_conflicted=Цей запит має зміни, що конфліктують з цільовою гілкою.
+pulls.is_checking=Перевірка конфліктів злиття ...
pulls.required_status_check_failed=Деякі необхідні перевірки виконані з помилками.
pulls.required_status_check_missing=Декілька з необхідних перевірок відсутні.
pulls.required_status_check_administrator=Як адміністратор ви все одно можете об'єднати цей запит на злиття.
@@ -1525,46 +1627,70 @@ pulls.status_checks_failure=Декілька перевірок не були у
pulls.status_checks_error=Декілька перевірок завершилися з помилками
pulls.status_checks_requested=Обов'язково
pulls.status_checks_details=Подробиці
+pulls.status_checks_hide_all=Приховати всі перевірки
+pulls.status_checks_show_all=Показати всі перевірки
pulls.update_branch=Оновити гілку шляхом злиття
pulls.update_branch_rebase=Оновити гілку перебазуванням
pulls.update_branch_success=Оновлення гілки пройшло успішно
pulls.update_not_allowed=Ви не можете оновити гілку
pulls.outdated_with_base_branch=Ця гілка застаріла відносно базової гілки
+pulls.close=Закрити запит на злиття
pulls.closed_at=`закрив цей запит на злиття %[2]s`
pulls.reopened_at=`повторно відкрив цей запит на злиття %[2]s`
+pulls.cmd_instruction_hint=Переглянути інструкції командного рядка
+pulls.cmd_instruction_merge_title=Об'єднати
+pulls.cmd_instruction_merge_desc=Об'єднати зміни і оновити на Gitea.
+pulls.clear_merge_message=Очистити повідомлення про злиття
+
+pulls.auto_merge_button_when_succeed=(Якщо перевірки успішні)
+pulls.auto_merge_when_succeed=Автоматичне злиття після успішного завершення всіх перевірок
+
+pulls.auto_merge_cancel_schedule=Скасувати автоматичне злиття
+pulls.auto_merge_not_scheduled=Автоматичне злиття цього запиту не заплановано.
+pulls.delete.title=Видалити цей запит на злиття?
+pulls.upstream_diverging_merge_confirm=Хочете об’єднати "%[1]s" з "%[2]s"?
-
-
+pull.deleted_branch=(видалена):%s
+pull.agit_documentation=Переглянути документацію про AGit
milestones.new=Новий етап
milestones.closed=Закрито %s
+milestones.update_ago=Оновлено %s
milestones.no_due_date=Немає дати завершення
milestones.open=Відкрити
milestones.close=Закрити
+milestones.completeness=%d%% завершено
milestones.create=Створити етап
milestones.title=Заголовок
milestones.desc=Опис
milestones.due_date=Дата завершення (опціонально)
milestones.clear=Очистити
milestones.invalid_due_date_format=Дата завершення має бути в форматі 'рррр-мм-дд'.
+milestones.create_success=Етап "%s" створено.
milestones.edit=Редагувати етап
milestones.edit_subheader=Створюйте етапи для організації ваших задач.
milestones.cancel=Відмінити
milestones.modify=Оновити етап
+milestones.edit_success=Етап '%s' оновлено.
milestones.deletion=Видалити етап
milestones.deletion_desc=Видалення етапу призведе до його видалення з усіх пов'язаних задач. Продовжити?
milestones.deletion_success=Етап успішно видалено.
milestones.filter_sort.name=Назва
-milestones.filter_sort.least_complete=Менш повне
-milestones.filter_sort.most_complete=Більш повне
+milestones.filter_sort.earliest_due_data=Найраніший строк
+milestones.filter_sort.latest_due_date=Останній строк
milestones.filter_sort.most_issues=Найбільш задач
milestones.filter_sort.least_issues=Найменш задач
+signing.will_sign=Цей коміт буде підписано ключем "%s".
+signing.wont_sign.nokey=Немає ключа для підписання цього коміту.
+signing.wont_sign.never=Коміти ніколи не підписуються.
+signing.wont_sign.always=Коміти завжди підписуються.
+ext_wiki=Доступ до зовнішньої вікі
ext_wiki.desc=Посилання на зовнішню вікі.
wiki=Вікі
@@ -1575,6 +1701,8 @@ wiki.create_first_page=Створити першу сторінку
wiki.page=Сторінка
wiki.filter_page=Фільтр сторінок
wiki.new_page=Сторінка
+wiki.page_title=Заголовок сторінки
+wiki.page_content=Зміст сторінки
wiki.default_commit_message=Напишіть примітку про оновлення цієї сторінки (необов'язково).
wiki.save_page=Зберегти сторінку
wiki.last_commit_info=%s редагував цю сторінку %s
@@ -1584,12 +1712,16 @@ wiki.file_revision=Ревізії сторінки
wiki.wiki_page_revisions=Ревізії вікі сторінок
wiki.back_to_wiki=Повернутись на сторінку Вікі
wiki.delete_page_button=Видалити сторінку
+wiki.delete_page_notice_1=Видалення вікі-сторінки "%s" не може бути скасовано. Продовжити?
wiki.page_already_exists=Вікі-сторінка з таким самим ім'ям вже існує.
+wiki.reserved_page=Назва сторінки вікі "%s" зарезервована.
wiki.pages=Сторінки
wiki.last_updated=Останні оновлення %s
wiki.page_name_desc=Введіть назву вікі-сторінки. Деякі із спеціальних імен: 'Home', '_Sidebar' та '_Footer'.
activity=Активність
+activity.navbar.pulse=Пульс
+activity.navbar.code_frequency=Частота коду
activity.period.filter_label=Період:
activity.period.daily=1 день
activity.period.halfweekly=3 дні
@@ -1655,7 +1787,9 @@ activity.git_stats_and_deletions=та
activity.git_stats_deletion_1=%d видалений
activity.git_stats_deletion_n=%d видалені
+contributors.contribution_type.filter_label=Тип внеску:
contributors.contribution_type.commits=Коміти
+contributors.contribution_type.deletions=Видалення
settings=Налаштування
settings.desc=У налаштуваннях ви можете змінювати різні параметри цього репозиторія
@@ -1671,6 +1805,7 @@ settings.hooks=Веб-хуки
settings.githooks=Git хуки
settings.basic_settings=Базові налаштування
settings.mirror_settings=Налаштування дзеркала
+settings.mirror_settings.docs.doc_link_title=Як віддзеркалити сховища?
settings.mirror_settings.mirrored_repository=Віддзеркалений репозиторій
settings.mirror_settings.direction=Напрямок
settings.mirror_settings.direction.pull=Pull
@@ -1683,7 +1818,10 @@ settings.mirror_settings.push_mirror.add=Додати Push дзеркало
settings.sync_mirror=Синхронізувати зараз
settings.site=Веб-сайт
settings.update_settings=Оновити налаштування
+settings.update_mirror_settings=Оновити параметри дзеркала
+settings.branches.switch_default_branch=Змінити типову гілку
settings.branches.update_default_branch=Оновити гілку за замовчуванням
+settings.branches.add_new_rule=Додати нове правило
settings.advanced_settings=Додаткові налаштування
settings.wiki_desc=Увімкнути репозиторії Вікі
settings.use_internal_wiki=Використовувати вбудовані Вікі
@@ -1702,6 +1840,9 @@ settings.tracker_url_format_error=Неправильний формат URL-ад
settings.tracker_issue_style=Формат номеру для зовнішньої системи обліку задач
settings.tracker_issue_style.numeric=Цифровий
settings.tracker_issue_style.alphanumeric=Буквено-цифровий
+settings.tracker_issue_style.regexp=Регулярний вираз
+settings.tracker_issue_style.regexp_pattern=Шаблон регулярного виразу
+settings.tracker_issue_style.regexp_pattern_desc=Першу захоплену групу буде використано замість {index}.
settings.tracker_url_format_desc=Використовуйте шаблони {user}, {repo} та {index} для імені користувача, репозиторію та номеру задічі.
settings.enable_timetracker=Увімкнути відстеження часу
settings.allow_only_contributors_to_track_time=Враховувати тільки учасників розробки в підрахунку часу
@@ -1709,9 +1850,19 @@ settings.pulls_desc=Увімкнути запити на злиття в реп
settings.pulls.ignore_whitespace=Ігнорувати пробіл у конфліктах
settings.pulls.enable_autodetect_manual_merge=Увімкнути автовизначення ручного злиття (Примітка: у деяких особливий випадках можуть виникнуть помилки)
settings.pulls.default_delete_branch_after_merge=Видаляти гілку запиту злиття, коли його прийнято
+settings.releases_desc=Увімкнути релізи сховища
+settings.projects_desc=Увімкнути проєкти
+settings.projects_mode_repo=Тільки проєкти сховища
+settings.projects_mode_owner=Тільки проєкти користувачів або організацій
settings.projects_mode_all=Всі проєкти
settings.admin_settings=Налаштування адміністратора
settings.admin_enable_health_check=Включити перевірки працездатності репозиторію (git fsck)
+settings.admin_code_indexer=Індексатор коду
+settings.admin_stats_indexer=Індексатор статистики коду
+settings.admin_indexer_commit_sha=Останній індексований SHA
+settings.admin_indexer_unindexed=Не індексовано
+settings.reindex_button=Додати до черги на реіндексацію
+settings.reindex_requested=Запит на реіндексацію
settings.admin_enable_close_issues_via_commit_in_any_branch=Закрити задачу за допомогою коміта, зробленого не в головній гілці
settings.danger_zone=Небезпечна зона
settings.new_owner_has_same_repo=Новий власник вже має репозиторій з такою назвою. Будь ласка, виберіть інше ім'я.
@@ -1822,6 +1973,8 @@ settings.event_delete_desc=Гілку або мітку було видален
settings.event_fork=Форк
settings.event_fork_desc=Репозиторій було форкнуто.
settings.event_wiki=Вікі
+settings.event_statuses=Статуси
+settings.event_statuses_desc=Статус коміту оновлено з API.
settings.event_release=Реліз
settings.event_release_desc=Реліз опублікований, оновлений або видалений з репозиторія.
settings.event_push=Push
@@ -1866,6 +2019,21 @@ settings.hook_type=Тип хука
settings.slack_token=Токен
settings.slack_domain=Домен
settings.slack_channel=Канал
+settings.web_hook_name_gitea=Gitea
+settings.web_hook_name_gogs=Gogs
+settings.web_hook_name_slack=Slack
+settings.web_hook_name_discord=Discord
+settings.web_hook_name_dingtalk=DingTalk
+settings.web_hook_name_telegram=Telegram
+settings.web_hook_name_matrix=Matrix
+settings.web_hook_name_msteams=Microsoft Teams
+settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
+settings.web_hook_name_feishu=Feishu
+settings.web_hook_name_larksuite=Lark Suite
+settings.web_hook_name_wechatwork=WeCom (Wechat Work)
+settings.web_hook_name_packagist=Packagist
+settings.packagist_username=Ім'я користувача Packagist
+settings.packagist_api_token=Токен API
settings.deploy_keys=Ключі для розгортування
settings.add_deploy_key=Додати ключ для розгортування
settings.deploy_key_desc=Ключі розгортання доступні тільки для читання. Це не те ж саме що і SSH-ключі аккаунта.
@@ -1881,8 +2049,11 @@ settings.deploy_key_deletion_desc=Видалення ключа розгортк
settings.deploy_key_deletion_success=Ключі розгортання було видалено.
settings.branches=Гілки
settings.protected_branch=Захист гілки
+settings.protected_branch.save_rule=Зберегти правило
+settings.protected_branch.delete_rule=Видалити правило
settings.protected_branch_can_push=Дозволити push?
settings.protected_branch_can_push_yes=Ви можете виконувати push
+settings.branch_protection=Захист гілки '%s'
settings.protect_this_branch=Захистити цю гілку
settings.protect_this_branch_desc=Запобігає видаленню гілки та обмежує виконання в ній push та злиття.
settings.protect_disable_push=Заборонити Push
@@ -1890,7 +2061,11 @@ settings.protect_disable_push_desc=Для цієї гілки буде забо
settings.protect_enable_push=Дозволити Push
settings.protect_enable_push_desc=Будь-хто із правом запису зможе виконувати push для цієї гілки (за виключенням force push).
settings.protect_check_status_contexts=Увімкнути перевірку стану
+settings.protect_status_check_patterns=Шаблони перевірки стану:
settings.protect_check_status_contexts_list=Перевірки статусу знайдено для репозитарію за минулий тиждень
+settings.protect_status_check_matched=Збіг
+settings.protect_invalid_status_check_pattern=Недійсний шаблон перевірки стану: "%s".
+settings.protect_no_valid_status_check_patterns=Немає дійсних шаблонів перевірки стану.
settings.protect_required_approvals=Необхідно схвалення:
settings.dismiss_stale_approvals=Відхилити застарілі погодження
settings.dismiss_stale_approvals_desc=Коли нові коміти що змінюють вміст пулл-запиту відправляються в гілку, старі погодження будуть відхилені.
@@ -1924,7 +2099,6 @@ settings.chat_id=Чат ID
settings.matrix.homeserver_url=URL домашньої сторінки
settings.matrix.room_id=Номер кімнати
settings.matrix.message_type=Тип повідомлення
-settings.archive.button=Архівний репозиторій
settings.archive.header=Відправити репозиторій в архів
settings.archive.success=Репозиторію успішно присвоєно статус архівного.
settings.archive.error=Сталася помилка при спробі архівувати репозиторій. Докладнішу інформацію див. у журналі.
@@ -2012,6 +2186,11 @@ diff.protected=Захищений
diff.image.side_by_side=Пліч-о-пліч
diff.image.swipe=Свайп
diff.image.overlay=Оверлей
+diff.show_file_tree=Показати дерево файлів
+diff.hide_file_tree=Сховати дерево файлів
+diff.submodule_added=Підмодуль %[1]s додано в %[2]s
+diff.submodule_deleted=Підмодуль %[1]s видалено з %[2]s
+diff.submodule_updated=Підмодуль %[1]s оновлено: %[2]s
releases.desc=Відслідковувати версії проєкту і завантаження.
release.releases=Релізи
@@ -2031,6 +2210,9 @@ release.edit_subheader=Публікація релізів допоможе ва
release.tag_name=Назва тегу
release.target=Ціль
release.tag_helper=Виберіть існуючий тег або створіть новий.
+release.title=Назва релізу
+release.title_empty=Заголовок не може бути порожнім.
+release.message=Опишіть цей реліз
release.prerelease_desc=Позначити як пре-реліз
release.prerelease_helper=Позначте цей випуск непридатним для ПРОД використання.
release.cancel=Відмінити
@@ -2051,33 +2233,57 @@ release.downloads=Завантажити
release.download_count=Завантаження: %s
release.add_tag_msg=Використовуйте заголовок і зміст релізу як повідомлення як тег повідомлення.
release.add_tag=Створити тільки мітку
+release.releases_for=Релізи для %s
branch.name=Ім'я гілки
+branch.already_exists=Гілка з назвою "%s" вже існує.
branch.delete_head=Видалити
+branch.delete=`Видалити гілку "%s"`
branch.delete_html=Видалити гілку
+branch.deletion_success=Гілку "%s" видалено.
+branch.deletion_failed=Не вдалося видалити гілку "%s".
branch.create_branch=Створити гілку %s
+branch.create_from=`з "%s"`
+branch.create_success=Створено гілку "%s".
branch.deleted_by=Видалено %s
+branch.restore_success=Гілку "%s" відновлено.
+branch.restore_failed=Не вдалося відновити гілку "%s".
+branch.protected_deletion_failed=Гілка "%s" захищена. Її неможливо видалити.
+branch.restore=`Відновити гілку "%s"`
+branch.download=`Завантажити гілку "%s"`
+branch.rename=`Перейменувати гілку "%s"`
branch.included_desc=Ця гілка є частиною типової гілки
branch.included=Включено
branch.create_new_branch=Створити гілку з гілки:
branch.confirm_create_branch=Створити гілку
+branch.rename_branch_to=Перейменувати "%s" на:
branch.confirm_rename_branch=Перейменувати гілку
branch.create_branch_operation=Створити гілку
branch.new_branch=Створити нову гілку
+branch.new_branch_from=`Створити нову гілку з "%s"`
branch.renamed=Гілку %s перейменовано на %s.
+branch.rename_default_or_protected_branch_error=Лише адміністратори можуть перейменовувати типові або захищені гілки.
tag.create_tag=Створити тег %s
topic.manage_topics=Керувати тематичними мітками
topic.done=Готово
+topic.count_prompt=Ви не можете вибрати більше ніж 25 тем
+topic.format_prompt=Теми мають починатися з літери або цифри, можуть містити дефіси ('-') і крапки ('.'), мати довжину до 35 символів. Літери повинні бути малими.
+find_file.go_to_file=Перейти до файлу
error.csv.too_large=Не вдається відобразити цей файл, тому що він завеликий.
error.csv.unexpected=Не вдається відобразити цей файл, тому що він містить неочікуваний символ в рядку %d і стовпці %d.
error.csv.invalid_field_count=Не вдається відобразити цей файл, тому що він має неправильну кількість полів у рядку %d.
[graphs]
+component_loading=Завантаження %s...
+component_loading_failed=Не вдалося завантажити %s
+component_failed_to_load=Сталась непередбачена помилка.
+code_frequency.what=частота коду
+contributors.what=внески
[org]
org_name_holder=Назва організації
@@ -2102,11 +2308,13 @@ team_permission_desc=Права доступу
team_unit_desc=Дозволити доступ до розділів репозиторію
team_unit_disabled=(Вимкнено)
+form.name_reserved=Назву організації "%s" зарезервовано.
form.create_org_not_allowed=Вам не дозволено створювати організації.
settings=Налаштування
settings.options=Організація
settings.full_name=Повне ім'я
+settings.email=Контактна адреса електронної пошти
settings.website=Веб-сайт
settings.location=Розташування
settings.permission=Дозволи
@@ -2152,8 +2360,10 @@ teams.leave.detail=Покинути %s?
teams.can_create_org_repo=Створити репозиторії
teams.can_create_org_repo_helper=Учасники можуть створювати нові репозиторії в організації. Автор отримає доступ адміністратора до нового репозиторію.
teams.none_access=Немає доступу
-teams.read_access=Прочитані
+teams.general_access=Загальний доступ
+teams.read_access=Читання
teams.read_access_helper=Учасники можуть переглядати та клонувати репозиторії команд.
+teams.write_access=Запис
teams.write_access_helper=Учасники можуть читати і виконувати push в репозиторії команд.
teams.admin_access=Доступ адміністратора
teams.admin_access_helper=Учасники можуть виконувати pull, push в репозиторії команд і додавати співавторів в команду.
@@ -2164,6 +2374,7 @@ teams.members=Учасники команди
teams.update_settings=Оновити налаштування
teams.delete_team=Видалити команду
teams.add_team_member=Додати учасника команди
+teams.invite_team_member=Запросити до %s
teams.delete_team_title=Видалити команду
teams.delete_team_desc=Видалення команди скасовує доступ до репозиторія для її учасників. Продовжити?
teams.delete_team_success=Команду було видалено.
@@ -2179,6 +2390,7 @@ teams.add_all_repos_desc=Це додасть всі репозиторії ор
teams.add_duplicate_users=Користувач уже є членом команди.
teams.repos.none=Для команди немає доступних репозиторіїв.
teams.members.none=Немає членів в цій команді.
+teams.members.blocked_user=Не вдається додати користувача, оскільки він заблокований організацією.
teams.specific_repositories=Конкретні репозиторії
teams.specific_repositories_helper=Учасники матимуть доступ лише до репозиторіїв, які були явно додані до команди. Вибір цього пункту не призводить до автоматичного видалення репозиторіїв, доданих з Всі репозиторії.
teams.all_repositories=Всі репозиторії
@@ -2186,15 +2398,30 @@ teams.all_repositories_helper=Команда має доступ до всіх
teams.all_repositories_read_permission_desc=Ця команда надає дозвіл Перегляд для всіх репозиторіїв: учасники можуть переглядати та клонувати їх.
teams.all_repositories_write_permission_desc=Ця команда надає дозвіл Запис для всіх репозиторіїв: учасники можуть переглядати та виконувати push в репозиторіях.
teams.all_repositories_admin_permission_desc=Ця команда надає дозвіл Адміністрування для всіх репозиторіїв: учасники можуть переглядати, виконувати push та додавати співробітників.
+teams.invite.title=Вас запросили приєднатися до команди %s в організації %s.
+teams.invite.description=Будь ласка, натисніть на кнопку нижче, щоб приєднатися до команди.
+view_as_role=Переглянути як: %s
+view_as_public_hint=Ви переглядаєте README як публічний користувач.
+view_as_member_hint=Ви переглядаєте README як член цієї організації.
+worktime=Час роботи
+worktime.date_range_start=Дата початку
+worktime.date_range_end=Дата завершення
+worktime.query=Запит
+worktime.time=Час
+worktime.by_milestones=За етапами
[admin]
+maintenance=Технічне обслуговування
dashboard=Панель управління
+self_check=Самоперевірка
users=Облікові записи користувачів
organizations=Організації
+assets=Ресурси коду
repositories=Репозиторії
hooks=Веб-хуки
+integrations=Інтеграції
authentication=Джерела автентифікації
emails=Електронні адреси Користувача
config=Конфігурація
@@ -2205,6 +2432,7 @@ monitor=Моніторинг
first_page=Перша
last_page=Остання
total=Разом: %d
+settings=Адміністративні налаштування
dashboard.statistic=Підсумок
dashboard.system_status=Статус системи
@@ -2271,6 +2499,10 @@ dashboard.total_gc_time=Загальна пауза збирача сміття
dashboard.total_gc_pause=Загальна пауза збирача сміття (GC)
dashboard.last_gc_pause=Остання пауза збирача сміття (GC)
dashboard.gc_times=Кількість запусків збирача сміття (GC)
+dashboard.update_checker=Перевірка оновлень
+dashboard.sync_branch.started=Розпочато синхронізацію гілок
+dashboard.rebuild_issue_indexer=Перебудувати індексатор задач
+dashboard.sync_repo_licenses=Синхронізувати ліцензії сховища
users.user_manage_panel=Керування обліковими записами користувачів
users.new_account=Створити обліковий запис
@@ -2279,12 +2511,15 @@ users.full_name=Повне ім'я
users.activated=Активовано
users.admin=Адміністратор
users.restricted=Обмежено
+users.reserved=Зарезервовано
+users.bot=Бот
users.2fa=2FA
users.repos=Репозиторії
users.created=Створено
users.last_login=Останній вхід
users.never_login=Ніколи не входив
users.send_register_notify=Надіслати повідомлення про реєстрацію користувача
+users.new_success=Обліковий запис "%s" створено.
users.edit=Редагувати
users.auth_source=Джерело автентифікації
users.local=Локальні
@@ -2304,8 +2539,10 @@ users.allow_import_local=Може імпортувати локальні реп
users.allow_create_organization=Може створювати організацій
users.update_profile=Оновити обліковий запис
users.delete_account=Видалити цей обліковий запис
+users.cannot_delete_self=Ви не можете видалити себе
users.still_own_repo=Ваш обліковий запис все ще володіє одним або кількома репозиторіями, спочатку вам потрібно видалити або передати їх.
users.still_has_org=Цей обліковий запис все ще є учасником однієї або декількох організацій. Для продовження, покиньте або видаліть організації.
+users.purge=Видалити користувача
users.deletion_success=Обліковий запис користувача було видалено.
users.reset_2fa=Скинути 2FA
users.list_status_filter.menu_text=Фільтр
@@ -2320,6 +2557,7 @@ users.list_status_filter.is_prohibit_login=Вхід заборонено
users.list_status_filter.not_prohibit_login=Вхід дозволено
users.list_status_filter.is_2fa_enabled=2FA увімкнена
users.list_status_filter.not_2fa_enabled=2FA вимкнена
+users.details=Інформація про користувача
emails.email_manage_panel=Управління поштою користувача
emails.primary=Головний
@@ -2332,6 +2570,10 @@ emails.updated=Електронну пошту оновлено
emails.not_updated=Не вдалось оновити адресу електронної пошти: %v
emails.duplicate_active=Ця електронна адреса вже активна для іншого користувача.
emails.change_email_header=Редагувати властивості електронної пошти
+emails.change_email_text=Ви впевнені, що хочете оновити адресу електронної пошти?
+emails.delete_desc=Ви впевнені, що бажаєте видалити цю електронну адресу?
+emails.deletion_success=Електронну адресу видалено.
+emails.delete_primary_email_error=Ви не можете видалити основну електронну адресу.
orgs.org_manage_panel=Керування організаціями
orgs.name=Назва
@@ -2347,12 +2589,20 @@ repos.name=Назва
repos.private=Приватний
repos.issues=Задачі
repos.size=Розмір
+repos.lfs_size=Розмір LFS
+packages.total_size=Загальний розмір: %s
+packages.unreferenced_size=Розмір без посилань: %s
+packages.cleanup=Очистити прострочені дані
+packages.cleanup.success=Очищення прострочених даних завершено
packages.owner=Власник
+packages.creator=Автор
packages.name=Назва
+packages.version=Версія
packages.type=Тип
-packages.repository=Репозиторій
+packages.repository=Сховище
packages.size=Розмір
+packages.published=Опубліковано
defaulthooks=Веб-хуки за замовчуванням
defaulthooks.add_webhook=Додати веб-хук за замовчуванням
@@ -2385,6 +2635,7 @@ auths.attribute_name=Атрибут імені
auths.attribute_surname=Атрибут Surname
auths.attribute_mail=Атрибут Email
auths.attribute_ssh_public_key=Атрибут Відкритий SSH ключ
+auths.attribute_avatar=Властивості аватару
auths.attributes_in_bind=Витягувати атрибути в контексті Bind DN
auths.allow_deactivate_all=Дозволити порожньому результату пошуку відключити всіх користувачів
auths.use_paged_search=Використовувати посторінковий пошук
@@ -2396,6 +2647,7 @@ auths.restricted_filter_helper=Залиште пустим, щоб не вста
auths.group_search_base=Пошукова база груп DN
auths.group_attribute_list_users=Атрибут групи зі списком користувачів
auths.user_attribute_in_group=Атрибути користувача в групі
+auths.enable_ldap_groups=Увімкнути групи LDAP
auths.ms_ad_sa=Атрибути пошуку MS AD
auths.smtp_auth=Тип автентифікації SMTP
auths.smtphost=SMTP хост
@@ -2438,9 +2690,15 @@ auths.tips=Поради
auths.tips.oauth2.general=OAuth2 автентифікація
auths.tip.oauth2_provider=Постачальник OAuth2
auths.tip.nextcloud=`Зареєструйте нового споживача OAuth у вашому екземплярі за допомогою наступного меню "Налаштування -> Безпека -> клієнт OAuth 2.0"`
+auths.tip.dropbox=Створити новий додаток на %s
+auths.tip.gitlab_new=Зареєструвати новий додаток на %s
+auths.tip.google_plus=Отримайте облікові дані клієнта OAuth2 з консолі Google API за адресою %s
+auths.tip.discord=Зареєструвати новий додаток на %s
+auths.tip.gitea=Зареєструйте новий додаток OAuth2. Посібник можна знайти за посиланням %s
auths.tip.mastodon=Введіть URL спеціального екземпляра для екземпляра mastodon, який ви хочете автентифікувати за допомогою (або використовувати за замовчуванням)
auths.edit=Редагувати джерело автентифікації
auths.activated=Ця аутентифікація активована
+auths.new_success=Автентифікацію "%s" додано.
auths.update_success=Параметри аутентифікації оновлені.
auths.update=Оновити джерело автентифікації
auths.delete=Видалити джерело автентифікації
@@ -2448,6 +2706,7 @@ auths.delete_auth_title=Видалити джерело автентифікац
auths.delete_auth_desc=Це джерело аутентифікації буде видалене, ви впевнені, що ви хочете продовжити?
auths.still_in_used=Ця перевірка справжності досі використовується деякими користувачами. Видаліть або змініть для цих користувачів тип входу в систему.
auths.deletion_success=Канал аутентифікації успішно знищений.
+auths.login_source_exist=Джерело автентифікації "%s" вже існує.
auths.login_source_of_type_exist=Джерело автентифікації такого типу вже наявне.
config.server_config=Конфігурація сервера
@@ -2462,6 +2721,7 @@ config.disable_router_log=Вимкнути логування роутеру
config.run_user=Запуск від імені Користувача
config.run_mode=Режим виконання
config.git_version=Версія Git
+config.app_data_path=Шлях до даних додатка
config.repo_root_path=Кореневий шлях репозиторія
config.lfs_root_path=Кореневої шлях LFS
config.log_file_root_path=Шлях до лог файлу
@@ -2487,7 +2747,7 @@ config.db_config=Конфігурація бази даних
config.db_type=Тип
config.db_host=Хост
config.db_name=Ім'я
-config.db_user=Ім'я кристувача
+config.db_user=Ім'я користувача
config.db_schema=Схема
config.db_ssl_mode=SSL
config.db_path=Шлях
@@ -2519,9 +2779,13 @@ config.queue_length=Довжина черги
config.deliver_timeout=Затримка доставки
config.skip_tls_verify=Пропустити перевірку TLS
+config.mailer_config=Налаштування пошти
config.mailer_enabled=Увімкнено
+config.mailer_enable_helo=Увімкнути HELO
config.mailer_name=Ім'я
-config.mailer_smtp_port=SMTP порт
+config.mailer_protocol=Протокол
+config.mailer_smtp_addr=Адреса SMTP
+config.mailer_smtp_port=Порт SMTP
config.mailer_user=Користувач
config.mailer_use_sendmail=Використовувати Sendmail
config.mailer_sendmail_path=Шлях до Sendmail
@@ -2529,6 +2793,9 @@ config.mailer_sendmail_args=Додаткові аргументи до Sendmail
config.mailer_sendmail_timeout=Тайм-аут Sendmail
config.test_email_placeholder=Адреса електронної пошти (наприклад, test@example.com)
config.send_test_mail=Відправити тестового листа
+config.send_test_mail_submit=Надіслати
+config.test_mail_failed=Не вдалося надіслати тестовий лист "%s": %v
+config.test_mail_sent=Тестовий лист надіслано "%s".
config.oauth_config=Конфігурація OAuth
config.oauth_enabled=Увімкнено
@@ -2538,6 +2805,9 @@ config.cache_adapter=Адаптер кешу
config.cache_interval=Інтервал кешування
config.cache_conn=Підключення до кешу
config.cache_item_ttl=Час зберігання даних кешу
+config.cache_test=Перевірити кеш
+config.cache_test_failed=Не вдалося перевірити кеш: %v.
+config.cache_test_slow=Перевірка кешу успішна, але відповідь повільна: %s.
config.session_config=Конфігурація сесії
config.session_provider=Провайдер сесії
@@ -2566,11 +2836,14 @@ config.git_pull_timeout=Тайм-аут операції Pull
config.git_gc_timeout=Тайм-аут операції збирача сміття (GC)
config.log_config=Конфігурація журналу
+config.logger_name_fmt=Журнал: %s
config.disabled_logger=Вимкнено
config.access_log_mode=Режим доступу до журналу
config.xorm_log_sql=Журнал SQL
+config.set_setting_failed=Не вдалося встановити параметр %s
+monitor.stats=Статистика
monitor.cron=Завдання cron
monitor.name=Ім'я
@@ -2579,10 +2852,13 @@ monitor.next=Наступного разу
monitor.previous=Попереднього разу
monitor.execute_times=Кількість виконань
monitor.process=Запущені процеси
+monitor.download_diagnosis_report=Завантажити звіт діагностики
monitor.desc=Опис
monitor.start=Час початку
monitor.execute_time=Час виконання
+monitor.last_execution_result=Результат
monitor.process.cancel=Зупинити процес
+monitor.process.cancel_desc=Скасування процесу може призвести до втрати даних
monitor.process.children=Дочірні процеси
monitor.queues=Черги
@@ -2598,6 +2874,8 @@ monitor.queue.settings.maxnumberworkers.placeholder=Поточний %[1]d
monitor.queue.settings.maxnumberworkers.error=Максимальна кількість робочих потоків має бути числом
monitor.queue.settings.submit=Оновити налаштування
monitor.queue.settings.changed=Налаштування оновлено
+monitor.queue.settings.remove_all_items=Видалити все
+monitor.queue.settings.remove_all_items_done=Усі елементи черги видалено.
notices.system_notice_list=Сповіщення системи
notices.view_detail_header=Переглянути деталі повідомлення
@@ -2613,11 +2891,13 @@ notices.desc=Опис
notices.op=Оп.
notices.delete_success=Сповіщення системи були видалені.
+self_check.no_problem_found=Наразі проблем не виявлено.
+self_check.startup_warnings=Попередження під час запуску:
[action]
create_repo=створив(ла) репозиторій %s
rename_repo=репозиторій перейменовано з %[1]s на %[3]s
-commit_repo=надіслав зміни (push) до %[3]s о %[4]s
+commit_repo=надіслав зміни (push) до %[3]s в %[4]s
create_issue=`відкрив задачу %[3]s#%[2]s`
close_issue=`закрив задачу %[3]s#%[2]s`
reopen_issue=`повторно відкрив задачу %[3]s#%[2]s`
@@ -2659,7 +2939,6 @@ seconds=%d секунди
minutes=%d хвилини
hours=%d години
days=%d дні
-weeks=%d тижднів
months=%d місяці
years=%d роки
raw_seconds=секунди
@@ -2667,6 +2946,7 @@ raw_minutes=хвилини
[dropzone]
default_message=Перетягніть файли або натисніть тут, щоб завантажити.
+invalid_input_type=Неможливо завантажити файли цього типу.
file_too_big=Розмір файлу ({{filesize}} MB), що більше ніж максимальний розмір: ({{maxFilesize}} MB).
remove_file=Видалити файл
@@ -2697,14 +2977,90 @@ error.no_unit_allowed_repo=У вас немає доступу до жодног
error.unit_not_allowed=У вас немає доступу до жодного розділу цього репозитория.
[packages]
+no_metadata=Немає метаданих.
filter.type=Тип
+filter.type.all=Всі
+filter.no_result=Ваш фільтр не дав результатів.
+filter.container.tagged=З міткою
+filter.container.untagged=Без мітки
+installation=Встановлення
+requirements=Вимоги
+dependencies=Залежності
+keywords=Ключові слова
+details=Подробиці
details.author=Автор
+details.project_site=Сторінка проєкту
+details.repository_site=Сторінка сховища
+details.documentation_site=Сторінка документації
+details.license=Ліцензія
+assets=Ресурси
+versions=Версії
+versions.view_all=Переглянути все
+dependency.version=Версія
+search_in_external_registry=Шукати в %s
+alpine.registry=Налаштуйте цей реєстр, додавши URL у ваш файл /etc/apk/repositories:
+alpine.repository=Інформація про сховище
alpine.repository.branches=Гілки
alpine.repository.repositories=Репозиторії
+alpine.repository.architectures=Архітектури
+arch.repository=Інформація про сховище
arch.repository.repositories=Репозиторії
+arch.repository.architectures=Архітектури
+composer.dependencies=Залежності
conan.details.repository=Репозиторій
+conan.registry=Налаштувати реєстр із командного рядка:
+container.details.type=Тип зображення
+container.details.platform=Платформа
+container.pull=Завантажити образ з командного рядка:
+container.images=Образи
+container.multi_arch=ОС / Архітектура
+container.labels=Мітки
+container.labels.key=Ключ
container.labels.value=Значення
+cran.registry=Налаштуйте цей реєстр у вашому файлі Rprofile.site:
+debian.registry=Налаштувати реєстр із командного рядка:
+debian.registry.info=Оберіть $distribution та $component зі списку нижче.
+debian.repository=Інформація про сховище
+debian.repository.distributions=Дистрибутиви
+debian.repository.components=Компоненти
+debian.repository.architectures=Архітектури
+helm.registry=Налаштувати реєстр із командного рядка:
+maven.registry=Налаштуйте цей реєстр у файлі pom.xml вашого проєкту:
+maven.install2=Виконати з командного рядка:
+maven.download=Щоб завантажити залежність, виконайте в командному рядку:
+nuget.registry=Налаштувати реєстр із командного рядка:
+npm.dependencies=Залежності
+npm.dependencies.optional=Необовʼязкові залежності
+pypi.requires=Потрібен Python
+rpm.registry=Налаштувати реєстр із командного рядка:
+rpm.repository=Інформація про сховище
+rpm.repository.architectures=Архітектури
+rubygems.install2=або додайте до Gemfile:
+rubygems.required.ruby=Вимагає версію Ruby
+rubygems.required.rubygems=Вимагає версію RubyGem
+swift.registry=Налаштувати реєстр із командного рядка:
+vagrant.install=Щоб додати бокс Vagrant, виконайте наступну команду:
+settings.link.select=Обрати сховище
+settings.link.button=Оновити посилання на сховище
+owner.settings.cargo.title=Індекс реєстру Cargo
+owner.settings.cargo.initialize=Ініціалізувати індекс
+owner.settings.cargo.initialize.description=Для використання реєстру Cargo потрібне спеціальне сховище індексів Git. Використання цього параметра дозволить (повторно) створити та автоматично налаштувати сховище.
+owner.settings.cargo.initialize.error=Не вдалося ініціалізувати індекс Cargo: %v
+owner.settings.cargo.initialize.success=Індекс Cargo успішно створено.
+owner.settings.cargo.rebuild=Перебудувати індекс
+owner.settings.cargo.rebuild.error=Не вдалося перебудувати індекс Cargo: %v
+owner.settings.cargo.rebuild.success=Індекс Cargo успішно перебудовано.
+owner.settings.cleanuprules.add=Додати правило очищення
+owner.settings.cleanuprules.edit=Редагувати правило очищення
+owner.settings.cleanuprules.none=Правила очищення відсутні. Будь ласка, зверніться до документації.
+owner.settings.cleanuprules.preview=Попередній перегляд правила очищення
+owner.settings.cleanuprules.preview.none=Правило очищення не відповідає жодному пакету.
owner.settings.cleanuprules.enabled=Увімкнено
+owner.settings.cleanuprules.keep.count=Залишити найновіші
+owner.settings.cleanuprules.remove.title=Версії, які відповідають цим правилам, видаляються, якщо правило вище не вимагає їх збереження.
+owner.settings.cleanuprules.success.update=Правило очищення оновлено.
+owner.settings.cleanuprules.success.delete=Правило очищення видалено.
+owner.settings.chef.title=Реєстр Chef
[secrets]
@@ -2714,26 +3070,83 @@ creation.description=Опис
[actions]
+actions=Дії
+unit.desc=Керувати діями
+status.success=Успіх
+status.failure=Невдача
+status.cancelled=Скасовано
+status.skipped=Пропущено
+status.blocked=Заблоковано
+runners.status=Статус
runners.name=Назва
runners.owner_type=Тип
runners.description=Опис
+runners.labels=Мітки
+runners.task_list.no_tasks=Наразі завдань немає.
runners.task_list.run=Запустити
+runners.task_list.status=Статус
runners.task_list.repository=Репозиторій
runners.task_list.commit=Коміт
+runners.task_list.done_at=Завершено о
+runners.status.idle=Очікування
runners.status.active=Активний
+runners.version=Версія
+runners.reset_registration_token=Скинути реєстраційний токен
+runs.all_workflows=Всі робочі процеси
runs.commit=Коміт
+runs.pushed_by=завантажено
+runs.no_job=Робочий процес повинен містити принаймні одну задачу
+runs.status=Статус
+runs.status_no_select=Всі статуси
+runs.no_workflows.documentation=Для отримання додаткової інформації про Gitea Дії, перегляньте документацію.
+runs.empty_commit_message=(порожнє повідомлення коміту)
+runs.view_workflow_file=Перегляд файлу робочого процесу
+
+workflow.disable=Вимкнути робочий процес
+workflow.disable_success=Робочий процес '%s' успішно вимкнено.
+workflow.enable=Увімкнути робочий процес
+workflow.enable_success=Робочий процес '%s' успішно ввімкнено.
+workflow.run=Запустити робочий процес
+workflow.not_found=Робочий процес '%s' не знайдено.
+workflow.from_ref=Використати робочий процес з
+variables=Змінні
+variables.management=Керування змінними
+variables.creation=Додати змінну
+variables.none=Наразі немає змінних.
+variables.deletion=Видалити змінну
+variables.deletion.description=Видалення змінної є остаточним і не може бути скасоване. Продовжити?
+variables.description=Змінні будуть передані певним діям і не можуть бути прочитані інакше.
+variables.id_not_exist=Змінної з ідентифікатором %d не існує.
+variables.edit=Редагувати змінну
+variables.deletion.failed=Не вдалося видалити змінну.
+variables.deletion.success=Змінну видалено.
+variables.creation.failed=Не вдалося додати змінну.
+variables.creation.success=Змінну "%s" додано.
+variables.update.failed=Не вдалося відредагувати змінну.
+variables.update.success=Змінну відредаговано.
-
+logs.always_auto_scroll=Завжди автоматично прокручувати журнали
+logs.always_expand_running=Завжди розгортати поточні журнали
[projects]
+deleted.display_name=Видалений проєкт
+type-1.display_name=Індивідуальний проєкт
+type-3.display_name=Проєкт організації
+enter_fullscreen=Повноекранний режим
+exit_fullscreen=Вийти з повноекранного режиму
[git.filemode]
+changed_filemode=%[1]s → %[2]s
; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Тека
+normal_file=Звичайний файл
+executable_file=Виконуваний файл
symbolic_link=Символічне посилання
+submodule=Підмодуль
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index d35814d82a..285ce7666f 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1652,6 +1652,7 @@ issues.save=保存
issues.label_title=标签名称
issues.label_description=标签描述
issues.label_color=标签颜色
+issues.label_color_invalid=无效的颜色
issues.label_exclusive=互斥标签
issues.label_archive=归档标签
issues.label_archived_filter=显示存档标签
From e9f5105e9502c4cedd19b7f6dde02adee8caff2a Mon Sep 17 00:00:00 2001
From: TheFox0x7
Date: Tue, 10 Jun 2025 14:35:12 +0200
Subject: [PATCH 002/131] Migrate to urfave v3 (#34510)
migrate cli to urfave v3
add more cli tests
---------
Co-authored-by: wxiaoguang
---
assets/go-licenses.json | 16 +-
cmd/actions.go | 10 +-
cmd/admin.go | 41 ++-
cmd/admin_auth.go | 15 +-
cmd/admin_auth_ldap.go | 88 +++--
cmd/admin_auth_ldap_test.go | 54 +--
cmd/admin_auth_oauth.go | 62 ++--
cmd/admin_auth_oauth_test.go | 333 ++++++++++++++++++
...{admin_auth_stmp.go => admin_auth_smtp.go} | 62 ++--
cmd/admin_auth_smtp_test.go | 285 +++++++++++++++
cmd/admin_regenerate.go | 14 +-
cmd/admin_user.go | 12 +-
cmd/admin_user_change_password.go | 64 ++--
cmd/admin_user_change_password_test.go | 91 +++++
cmd/admin_user_create.go | 164 +++++----
cmd/admin_user_create_test.go | 7 +-
cmd/admin_user_delete.go | 67 ++--
cmd/admin_user_delete_test.go | 111 ++++++
cmd/admin_user_generate_access_token.go | 8 +-
cmd/admin_user_list.go | 8 +-
cmd/admin_user_must_change_password.go | 55 +--
cmd/admin_user_must_change_password_test.go | 78 ++++
cmd/cert.go | 124 ++++---
cmd/cert_test.go | 123 +++++++
cmd/cmd.go | 15 +-
cmd/docs.go | 18 +-
cmd/doctor.go | 48 ++-
cmd/doctor_convert.go | 10 +-
cmd/doctor_test.go | 13 +-
cmd/dump.go | 36 +-
cmd/dump_repo.go | 29 +-
cmd/embedded.go | 21 +-
cmd/generate.go | 13 +-
cmd/hook.go | 22 +-
cmd/keys.go | 10 +-
cmd/mailer.go | 12 +-
cmd/main.go | 59 ++--
cmd/main_test.go | 31 +-
cmd/manager.go | 30 +-
cmd/manager_logging.go | 49 +--
cmd/migrate.go | 9 +-
cmd/migrate_storage.go | 57 ++-
cmd/restore_repo.go | 8 +-
cmd/serv.go | 7 +-
cmd/web.go | 28 +-
contrib/backport/backport.go | 38 +-
.../environment-to-ini/environment-to-ini.go | 9 +-
go.mod | 6 +-
go.sum | 12 +-
main.go | 2 +-
tests/integration/cmd_keys_test.go | 17 +-
51 files changed, 1718 insertions(+), 783 deletions(-)
create mode 100644 cmd/admin_auth_oauth_test.go
rename cmd/{admin_auth_stmp.go => admin_auth_smtp.go} (74%)
create mode 100644 cmd/admin_auth_smtp_test.go
create mode 100644 cmd/admin_user_change_password_test.go
create mode 100644 cmd/admin_user_delete_test.go
create mode 100644 cmd/admin_user_must_change_password_test.go
create mode 100644 cmd/cert_test.go
diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 3827a092f1..d961444239 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -1080,9 +1080,14 @@
"licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
},
{
- "name": "github.com/urfave/cli/v2",
- "path": "github.com/urfave/cli/v2/LICENSE",
- "licenseText": "MIT License\n\nCopyright (c) 2022 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ "name": "github.com/urfave/cli-docs/v3",
+ "path": "github.com/urfave/cli-docs/v3/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/urfave/cli/v3",
+ "path": "github.com/urfave/cli/v3/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/valyala/fastjson",
@@ -1109,11 +1114,6 @@
"path": "github.com/xanzy/ssh-agent/LICENSE",
"licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n"
},
- {
- "name": "github.com/xrash/smetrics",
- "path": "github.com/xrash/smetrics/LICENSE",
- "licenseText": "Copyright (C) 2016 Felipe da Cunha Gonçalves\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
- },
{
"name": "github.com/yohcop/openid-go",
"path": "github.com/yohcop/openid-go/LICENSE",
diff --git a/cmd/actions.go b/cmd/actions.go
index f582c16c81..2c51c6a1bc 100644
--- a/cmd/actions.go
+++ b/cmd/actions.go
@@ -4,12 +4,13 @@
package cmd
import (
+ "context"
"fmt"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -17,7 +18,7 @@ var (
CmdActions = &cli.Command{
Name: "actions",
Usage: "Manage Gitea Actions",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdActionsGenRunnerToken,
},
}
@@ -38,10 +39,7 @@ var (
}
)
-func runGenerateActionsRunnerToken(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()
scope := c.String("scope")
diff --git a/cmd/admin.go b/cmd/admin.go
index 7f2d985169..559544edd3 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -23,7 +23,7 @@ var (
CmdAdmin = &cli.Command{
Name: "admin",
Usage: "Perform common administrative operations",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdUser,
subcmdRepoSyncReleases,
subcmdRegenerate,
@@ -41,7 +41,7 @@ var (
subcmdRegenerate = &cli.Command{
Name: "regenerate",
Usage: "Regenerate specific files",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
microcmdRegenHooks,
microcmdRegenKeys,
},
@@ -50,15 +50,15 @@ var (
subcmdAuth = &cli.Command{
Name: "auth",
Usage: "Modify external auth providers",
- Subcommands: []*cli.Command{
- microcmdAuthAddOauth,
- microcmdAuthUpdateOauth,
- microcmdAuthAddLdapBindDn,
- microcmdAuthUpdateLdapBindDn,
- microcmdAuthAddLdapSimpleAuth,
- microcmdAuthUpdateLdapSimpleAuth,
- microcmdAuthAddSMTP,
- microcmdAuthUpdateSMTP,
+ Commands: []*cli.Command{
+ microcmdAuthAddOauth(),
+ microcmdAuthUpdateOauth(),
+ microcmdAuthAddLdapBindDn(),
+ microcmdAuthUpdateLdapBindDn(),
+ microcmdAuthAddLdapSimpleAuth(),
+ microcmdAuthUpdateLdapSimpleAuth(),
+ microcmdAuthAddSMTP(),
+ microcmdAuthUpdateSMTP(),
microcmdAuthList,
microcmdAuthDelete,
},
@@ -70,9 +70,9 @@ var (
Action: runSendMail,
Flags: []cli.Flag{
&cli.StringFlag{
- Name: "title",
- Usage: `a title of a message`,
- Value: "",
+ Name: "title",
+ Usage: "a title of a message",
+ Required: true,
},
&cli.StringFlag{
Name: "content",
@@ -86,17 +86,16 @@ var (
},
},
}
+)
- idFlag = &cli.Int64Flag{
+func idFlag() *cli.Int64Flag {
+ return &cli.Int64Flag{
Name: "id",
Usage: "ID of authentication source",
}
-)
-
-func runRepoSyncReleases(_ *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
+}
+func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go
index 4777a92908..1a09366722 100644
--- a/cmd/admin_auth.go
+++ b/cmd/admin_auth.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"os"
@@ -13,14 +14,14 @@ import (
"code.gitea.io/gitea/models/db"
auth_service "code.gitea.io/gitea/services/auth"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
microcmdAuthDelete = &cli.Command{
Name: "delete",
Usage: "Delete specific auth source",
- Flags: []cli.Flag{idFlag},
+ Flags: []cli.Flag{idFlag()},
Action: runDeleteAuth,
}
microcmdAuthList = &cli.Command{
@@ -56,10 +57,7 @@ var (
}
)
-func runListAuth(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runListAuth(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
@@ -90,14 +88,11 @@ func runListAuth(c *cli.Context) error {
return nil
}
-func runDeleteAuth(c *cli.Context) error {
+func runDeleteAuth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
- ctx, cancel := installSignals()
- defer cancel()
-
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
index d2eeb7c0d6..069ad6600c 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/ldap"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
type (
@@ -24,8 +24,8 @@ type (
}
)
-var (
- commonLdapCLIFlags = []cli.Flag{
+func commonLdapCLIFlags() []cli.Flag {
+ return []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "Authentication name.",
@@ -103,8 +103,10 @@ var (
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
},
}
+}
- ldapBindDnCLIFlags = append(commonLdapCLIFlags,
+func ldapBindDnCLIFlags() []cli.Flag {
+ return append(commonLdapCLIFlags(),
&cli.StringFlag{
Name: "bind-dn",
Usage: "The DN to bind to the LDAP server with when searching for the user.",
@@ -157,49 +159,59 @@ var (
Name: "group-team-map-removal",
Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
})
+}
- ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
+func ldapSimpleAuthCLIFlags() []cli.Flag {
+ return append(commonLdapCLIFlags(),
&cli.StringFlag{
Name: "user-dn",
Usage: "The user's DN.",
})
+}
- microcmdAuthAddLdapBindDn = &cli.Command{
+func microcmdAuthAddLdapBindDn() *cli.Command {
+ return &cli.Command{
Name: "add-ldap",
Usage: "Add new LDAP (via Bind DN) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().addLdapBindDn(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().addLdapBindDn(ctx, cmd)
},
- Flags: ldapBindDnCLIFlags,
+ Flags: ldapBindDnCLIFlags(),
}
+}
- microcmdAuthUpdateLdapBindDn = &cli.Command{
+func microcmdAuthUpdateLdapBindDn() *cli.Command {
+ return &cli.Command{
Name: "update-ldap",
Usage: "Update existing LDAP (via Bind DN) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().updateLdapBindDn(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().updateLdapBindDn(ctx, cmd)
},
- Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...),
+ Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
}
+}
- microcmdAuthAddLdapSimpleAuth = &cli.Command{
+func microcmdAuthAddLdapSimpleAuth() *cli.Command {
+ return &cli.Command{
Name: "add-ldap-simple",
Usage: "Add new LDAP (simple auth) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().addLdapSimpleAuth(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().addLdapSimpleAuth(ctx, cmd)
},
- Flags: ldapSimpleAuthCLIFlags,
+ Flags: ldapSimpleAuthCLIFlags(),
}
+}
- microcmdAuthUpdateLdapSimpleAuth = &cli.Command{
+func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
+ return &cli.Command{
Name: "update-ldap-simple",
Usage: "Update existing LDAP (simple auth) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().updateLdapSimpleAuth(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().updateLdapSimpleAuth(ctx, cmd)
},
- Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...),
+ Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
}
-)
+}
// newAuthService creates a service with default functions.
func newAuthService() *authService {
@@ -212,7 +224,7 @@ func newAuthService() *authService {
}
// parseAuthSourceLdap assigns values on authSource according to command line flags.
-func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
+func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
if c.IsSet("name") {
authSource.Name = c.String("name")
}
@@ -232,7 +244,7 @@ func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
}
// parseLdapConfig assigns values on config according to command line flags.
-func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
+func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
if c.IsSet("name") {
config.Name = c.String("name")
}
@@ -245,7 +257,7 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("security-protocol") {
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
if !ok {
- return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
+ return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol"))
}
config.SecurityProtocol = p
}
@@ -337,32 +349,27 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
// getAuthSource gets the login source by its id defined in the command line flags.
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
-func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) {
+func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
if err := argsSet(c, "id"); err != nil {
return nil, err
}
-
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return nil, err
}
if authSource.Type != authType {
- return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
+ return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
}
return authSource, nil
}
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
-func (a *authService) addLdapBindDn(c *cli.Context) error {
+func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
return err
}
-
- ctx, cancel := installSignals()
- defer cancel()
-
if err := a.initDB(ctx); err != nil {
return err
}
@@ -384,10 +391,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
}
// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
-func (a *authService) updateLdapBindDn(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
@@ -406,14 +410,11 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
}
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
-func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
+func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
return err
}
- ctx, cancel := installSignals()
- defer cancel()
-
if err := a.initDB(ctx); err != nil {
return err
}
@@ -435,10 +436,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
}
// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
-func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
index 52ab78fe13..2da7ebc573 100644
--- a/cmd/admin_auth_ldap_test.go
+++ b/cmd/admin_auth_ldap_test.go
@@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/services/auth/source/ldap"
"github.com/stretchr/testify/assert"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func TestAddLdapBindDn(t *testing.T) {
@@ -134,7 +134,7 @@ func TestAddLdapBindDn(t *testing.T) {
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
"--email-attribute", "mail",
},
- errMsg: "Unknown security protocol name: zzzzz",
+ errMsg: "unknown security protocol name: zzzzz",
},
// case 3
{
@@ -238,12 +238,13 @@ func TestAddLdapBindDn(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthAddLdapBindDn.Flags
- app.Action = service.addLdapBindDn
+ app := cli.Command{
+ Flags: microcmdAuthAddLdapBindDn().Flags,
+ Action: service.addLdapBindDn,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -345,12 +346,12 @@ func TestAddLdapSimpleAuth(t *testing.T) {
"--name", "ldap (simple auth) source",
"--security-protocol", "zzzzz",
"--host", "ldap-server",
- "--port", "123",
+ "--port", "1234",
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
"--email-attribute", "mail",
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
},
- errMsg: "Unknown security protocol name: zzzzz",
+ errMsg: "unknown security protocol name: zzzzz",
},
// case 3
{
@@ -467,12 +468,13 @@ func TestAddLdapSimpleAuth(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthAddLdapSimpleAuth.Flags
- app.Action = service.addLdapSimpleAuth
+ app := &cli.Command{
+ Flags: microcmdAuthAddLdapSimpleAuth().Flags,
+ Action: service.addLdapSimpleAuth,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -859,7 +861,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
"--id", "1",
"--security-protocol", "xxxxx",
},
- errMsg: "Unknown security protocol name: xxxxx",
+ errMsg: "unknown security protocol name: xxxxx",
},
// case 22
{
@@ -878,7 +880,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
Type: auth.OAuth2,
Cfg: &ldap.Source{},
},
- errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
+ errMsg: "invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
},
// case 24
{
@@ -942,12 +944,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthUpdateLdapBindDn.Flags
- app.Action = service.updateLdapBindDn
-
+ app := cli.Command{
+ Flags: microcmdAuthUpdateLdapBindDn().Flags,
+ Action: service.updateLdapBindDn,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -1250,7 +1252,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
"--id", "1",
"--security-protocol", "xxxxx",
},
- errMsg: "Unknown security protocol name: xxxxx",
+ errMsg: "unknown security protocol name: xxxxx",
},
// case 18
{
@@ -1269,7 +1271,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
Type: auth.PAM,
Cfg: &ldap.Source{},
},
- errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
+ errMsg: "invalid authentication type. expected: LDAP (simple auth), actual: PAM",
},
// case 20
{
@@ -1330,12 +1332,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags
- app.Action = service.updateLdapSimpleAuth
-
+ app := cli.Command{
+ Flags: microcmdAuthUpdateLdapSimpleAuth().Flags,
+ Action: service.updateLdapSimpleAuth,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go
index be5345d992..d1aa753500 100644
--- a/cmd/admin_auth_oauth.go
+++ b/cmd/admin_auth_oauth.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"net/url"
@@ -12,11 +13,11 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var (
- oauthCLIFlags = []cli.Flag{
+func oauthCLIFlags() []cli.Flag {
+ return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
@@ -121,23 +122,34 @@ var (
Usage: "Activate automatic team membership removal depending on groups",
},
}
+}
- microcmdAuthAddOauth = &cli.Command{
- Name: "add-oauth",
- Usage: "Add new Oauth authentication source",
- Action: runAddOauth,
- Flags: oauthCLIFlags,
+func microcmdAuthAddOauth() *cli.Command {
+ return &cli.Command{
+ Name: "add-oauth",
+ Usage: "Add new Oauth authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runAddOauth(ctx, cmd)
+ },
+ Flags: oauthCLIFlags(),
}
+}
- microcmdAuthUpdateOauth = &cli.Command{
- Name: "update-oauth",
- Usage: "Update existing Oauth authentication source",
- Action: runUpdateOauth,
- Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
+func microcmdAuthUpdateOauth() *cli.Command {
+ return &cli.Command{
+ Name: "update-oauth",
+ Usage: "Update existing Oauth authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runUpdateOauth(ctx, cmd)
+ },
+ Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
+ Name: "id",
+ Usage: "ID of authentication source",
+ }}, oauthCLIFlags()[1:]...)...),
}
-)
+}
-func parseOAuth2Config(c *cli.Context) *oauth2.Source {
+func parseOAuth2Config(c *cli.Command) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping
if c.IsSet("use-custom-urls") {
customURLMapping = &oauth2.CustomURLMapping{
@@ -168,11 +180,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
}
}
-func runAddOauth(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
+ if err := a.initDB(ctx); err != nil {
return err
}
@@ -184,7 +193,7 @@ func runAddOauth(c *cli.Context) error {
}
}
- return auth_model.CreateSource(ctx, &auth_model.Source{
+ return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
@@ -193,19 +202,16 @@ func runAddOauth(c *cli.Context) error {
})
}
-func runUpdateOauth(c *cli.Context) error {
+func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+ if err := a.initDB(ctx); err != nil {
return err
}
- source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
+ source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
@@ -296,5 +302,5 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
- return auth_model.UpdateSource(ctx, source)
+ return a.updateAuthSource(ctx, source)
}
diff --git a/cmd/admin_auth_oauth_test.go b/cmd/admin_auth_oauth_test.go
new file mode 100644
index 0000000000..df1bd9c1a6
--- /dev/null
+++ b/cmd/admin_auth_oauth_test.go
@@ -0,0 +1,333 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "context"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/services/auth/source/oauth2"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/urfave/cli/v3"
+)
+
+func TestAddOauth(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ source *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "valid config",
+ args: []string{
+ "--name", "test",
+ "--provider", "github",
+ "--key", "some_key",
+ "--secret", "some_secret",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Scopes: []string{},
+ Provider: "github",
+ ClientID: "some_key",
+ ClientSecret: "some_secret",
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ {
+ name: "valid config with openid connect",
+ args: []string{
+ "--name", "test",
+ "--provider", "openidConnect",
+ "--key", "some_key",
+ "--secret", "some_secret",
+ "--auto-discover-url", "https://example.com",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Scopes: []string{},
+ Provider: "openidConnect",
+ ClientID: "some_key",
+ ClientSecret: "some_secret",
+ OpenIDConnectAutoDiscoveryURL: "https://example.com",
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ {
+ name: "valid config with options",
+ args: []string{
+ "--name", "test",
+ "--provider", "gitlab",
+ "--key", "some_key",
+ "--secret", "some_secret",
+ "--use-custom-urls", "true",
+ "--custom-token-url", "https://example.com/token",
+ "--custom-auth-url", "https://example.com/auth",
+ "--custom-profile-url", "https://example.com/profile",
+ "--custom-email-url", "https://example.com/email",
+ "--custom-tenant-id", "some_tenant",
+ "--icon-url", "https://example.com/icon",
+ "--scopes", "scope1,scope2",
+ "--skip-local-2fa", "true",
+ "--required-claim-name", "claim_name",
+ "--required-claim-value", "claim_value",
+ "--group-claim-name", "group_name",
+ "--admin-group", "admin",
+ "--restricted-group", "restricted",
+ "--group-team-map", `{"group1": [1,2]}`,
+ "--group-team-map-removal=true",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "gitlab",
+ ClientID: "some_key",
+ ClientSecret: "some_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{
+ TokenURL: "https://example.com/token",
+ AuthURL: "https://example.com/auth",
+ ProfileURL: "https://example.com/profile",
+ EmailURL: "https://example.com/email",
+ Tenant: "some_tenant",
+ },
+ IconURL: "https://example.com/icon",
+ Scopes: []string{"scope1", "scope2"},
+ RequiredClaimName: "claim_name",
+ RequiredClaimValue: "claim_value",
+ GroupClaimName: "group_name",
+ AdminGroup: "admin",
+ RestrictedGroup: "restricted",
+ GroupTeamMap: `{"group1": [1,2]}`,
+ GroupTeamMapRemoval: true,
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var createdSource *auth_model.Source
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ createdSource = source
+ return nil
+ },
+ }
+
+ app := &cli.Command{
+ Flags: microcmdAuthAddOauth().Flags,
+ Action: a.runAddOauth,
+ }
+
+ args := []string{"oauth-test"}
+ args = append(args, tc.args...)
+
+ err := app.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.source, createdSource)
+ }
+ })
+ }
+}
+
+func TestUpdateOauth(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ id int64
+ existingAuthSource *auth_model.Source
+ authSource *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "missing id",
+ args: []string{
+ "--name", "test",
+ },
+ errMsg: "--id flag is missing",
+ },
+ {
+ name: "valid config",
+ id: 1,
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "github",
+ ClientID: "old_key",
+ ClientSecret: "old_secret",
+ },
+ TwoFactorPolicy: "",
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--provider", "gitlab",
+ "--key", "new_key",
+ "--secret", "new_secret",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "gitlab",
+ ClientID: "new_key",
+ ClientSecret: "new_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{},
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ {
+ name: "valid config with options",
+ id: 1,
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "gitlab",
+ ClientID: "old_key",
+ ClientSecret: "old_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{
+ TokenURL: "https://old.example.com/token",
+ AuthURL: "https://old.example.com/auth",
+ ProfileURL: "https://old.example.com/profile",
+ EmailURL: "https://old.example.com/email",
+ Tenant: "old_tenant",
+ },
+ IconURL: "https://old.example.com/icon",
+ Scopes: []string{"old_scope1", "old_scope2"},
+ RequiredClaimName: "old_claim_name",
+ RequiredClaimValue: "old_claim_value",
+ GroupClaimName: "old_group_name",
+ AdminGroup: "old_admin",
+ RestrictedGroup: "old_restricted",
+ GroupTeamMap: `{"old_group1": [1,2]}`,
+ GroupTeamMapRemoval: true,
+ },
+ TwoFactorPolicy: "",
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--provider", "github",
+ "--key", "new_key",
+ "--secret", "new_secret",
+ "--use-custom-urls", "true",
+ "--custom-token-url", "https://example.com/token",
+ "--custom-auth-url", "https://example.com/auth",
+ "--custom-profile-url", "https://example.com/profile",
+ "--custom-email-url", "https://example.com/email",
+ "--custom-tenant-id", "new_tenant",
+ "--icon-url", "https://example.com/icon",
+ "--scopes", "scope1,scope2",
+ "--skip-local-2fa=true",
+ "--required-claim-name", "claim_name",
+ "--required-claim-value", "claim_value",
+ "--group-claim-name", "group_name",
+ "--admin-group", "admin",
+ "--restricted-group", "restricted",
+ "--group-team-map", `{"group1": [1,2]}`,
+ "--group-team-map-removal=false",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "github",
+ ClientID: "new_key",
+ ClientSecret: "new_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{
+ TokenURL: "https://example.com/token",
+ AuthURL: "https://example.com/auth",
+ ProfileURL: "https://example.com/profile",
+ EmailURL: "https://example.com/email",
+ Tenant: "new_tenant",
+ },
+ IconURL: "https://example.com/icon",
+ Scopes: []string{"scope1", "scope2"},
+ RequiredClaimName: "claim_name",
+ RequiredClaimValue: "claim_value",
+ GroupClaimName: "group_name",
+ AdminGroup: "admin",
+ RestrictedGroup: "restricted",
+ GroupTeamMap: `{"group1": [1,2]}`,
+ GroupTeamMapRemoval: false,
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
+ return &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ CustomURLMapping: &oauth2.CustomURLMapping{},
+ },
+ TwoFactorPolicy: "skip",
+ }, nil
+ },
+ updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ assert.Equal(t, tc.authSource, source)
+ return nil
+ },
+ }
+
+ app := &cli.Command{
+ Flags: microcmdAuthUpdateOauth().Flags,
+ Action: a.runUpdateOauth,
+ }
+
+ args := []string{"oauth-test"}
+ args = append(args, tc.args...)
+
+ err := app.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/cmd/admin_auth_stmp.go b/cmd/admin_auth_smtp.go
similarity index 74%
rename from cmd/admin_auth_stmp.go
rename to cmd/admin_auth_smtp.go
index babcf78cea..e9daf71809 100644
--- a/cmd/admin_auth_stmp.go
+++ b/cmd/admin_auth_smtp.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"strings"
@@ -11,11 +12,11 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/smtp"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var (
- smtpCLIFlags = []cli.Flag{
+func smtpCLIFlags() []cli.Flag {
+ return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
@@ -71,23 +72,34 @@ var (
Value: true,
},
}
+}
- microcmdAuthAddSMTP = &cli.Command{
- Name: "add-smtp",
- Usage: "Add new SMTP authentication source",
- Action: runAddSMTP,
- Flags: smtpCLIFlags,
+func microcmdAuthUpdateSMTP() *cli.Command {
+ return &cli.Command{
+ Name: "update-smtp",
+ Usage: "Update existing SMTP authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runUpdateSMTP(ctx, cmd)
+ },
+ Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
+ Name: "id",
+ Usage: "ID of authentication source",
+ }}, smtpCLIFlags()[1:]...)...),
}
+}
- microcmdAuthUpdateSMTP = &cli.Command{
- Name: "update-smtp",
- Usage: "Update existing SMTP authentication source",
- Action: runUpdateSMTP,
- Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...),
+func microcmdAuthAddSMTP() *cli.Command {
+ return &cli.Command{
+ Name: "add-smtp",
+ Usage: "Add new SMTP authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runAddSMTP(ctx, cmd)
+ },
+ Flags: smtpCLIFlags(),
}
-)
+}
-func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
+func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
if c.IsSet("auth-type") {
conf.Auth = c.String("auth-type")
validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
@@ -120,11 +132,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
return nil
}
-func runAddSMTP(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
+ if err := a.initDB(ctx); err != nil {
return err
}
@@ -152,7 +161,7 @@ func runAddSMTP(c *cli.Context) error {
smtpConfig.Auth = "PLAIN"
}
- return auth_model.CreateSource(ctx, &auth_model.Source{
+ return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.SMTP,
Name: c.String("name"),
IsActive: active,
@@ -161,19 +170,16 @@ func runAddSMTP(c *cli.Context) error {
})
}
-func runUpdateSMTP(c *cli.Context) error {
+func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+ if err := a.initDB(ctx); err != nil {
return err
}
- source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
+ source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
@@ -194,5 +200,5 @@ func runUpdateSMTP(c *cli.Context) error {
source.Cfg = smtpConfig
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
- return auth_model.UpdateSource(ctx, source)
+ return a.updateAuthSource(ctx, source)
}
diff --git a/cmd/admin_auth_smtp_test.go b/cmd/admin_auth_smtp_test.go
new file mode 100644
index 0000000000..9778ff87d2
--- /dev/null
+++ b/cmd/admin_auth_smtp_test.go
@@ -0,0 +1,285 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "context"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/services/auth/source/smtp"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/urfave/cli/v3"
+)
+
+func TestAddSMTP(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ source *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "missing name",
+ args: []string{
+ "--host", "localhost",
+ "--port", "25",
+ },
+ errMsg: "name must be set",
+ },
+ {
+ name: "missing host",
+ args: []string{
+ "--name", "test",
+ "--port", "25",
+ },
+ errMsg: "host must be set",
+ },
+ {
+ name: "missing port",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ },
+ errMsg: "port must be set",
+ },
+ {
+ name: "valid config",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "localhost",
+ Port: 25,
+ // ForceSMTPS: true,
+ // SkipVerify: true,
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ {
+ name: "valid config with options",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ "--auth-type", "LOGIN",
+ "--force-smtps=false",
+ "--skip-verify=false",
+ "--helo-hostname", "example.com",
+ "--disable-helo=false",
+ "--allowed-domains", "example.com,example.org",
+ "--skip-local-2fa=false",
+ "--active=false",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: false,
+ Cfg: &smtp.Source{
+ Auth: "LOGIN",
+ Host: "localhost",
+ Port: 25,
+ ForceSMTPS: false,
+ SkipVerify: false,
+ HeloHostname: "example.com",
+ DisableHelo: false,
+ AllowedDomains: "example.com,example.org",
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ assert.Equal(t, tc.source, source)
+ return nil
+ },
+ }
+
+ cmd := &cli.Command{
+ Flags: microcmdAuthAddSMTP().Flags,
+ Action: a.runAddSMTP,
+ }
+
+ args := []string{"smtp-test"}
+ args = append(args, tc.args...)
+
+ t.Log(args)
+ err := cmd.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestUpdateSMTP(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ existingAuthSource *auth_model.Source
+ authSource *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "missing id",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ },
+ errMsg: "--id flag is missing",
+ },
+ {
+ name: "valid config",
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "old host",
+ Port: 26,
+ ForceSMTPS: true,
+ SkipVerify: true,
+ },
+ TwoFactorPolicy: "",
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "localhost",
+ Port: 25,
+ ForceSMTPS: true,
+ SkipVerify: true,
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ {
+ name: "valid config with options",
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "old host",
+ Port: 26,
+ ForceSMTPS: true,
+ SkipVerify: true,
+ HeloHostname: "old.example.com",
+ DisableHelo: false,
+ AllowedDomains: "old.example.com",
+ },
+ TwoFactorPolicy: "",
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ "--auth-type", "LOGIN",
+ "--force-smtps=false",
+ "--skip-verify=false",
+ "--helo-hostname", "example.com",
+ "--disable-helo=true",
+ "--allowed-domains", "example.com,example.org",
+ "--skip-local-2fa=true",
+ "--active=false",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: false,
+ Cfg: &smtp.Source{
+ Auth: "LOGIN",
+ Host: "localhost",
+ Port: 25,
+ ForceSMTPS: false,
+ SkipVerify: false,
+ HeloHostname: "example.com",
+ DisableHelo: true,
+ AllowedDomains: "example.com,example.org",
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
+ return &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ SkipVerify: true,
+ ForceSMTPS: true,
+ },
+ TwoFactorPolicy: "skip",
+ }, nil
+ },
+
+ updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ assert.Equal(t, tc.authSource, source)
+ return nil
+ },
+ }
+
+ app := &cli.Command{
+ Flags: microcmdAuthUpdateSMTP().Flags,
+ Action: a.runUpdateSMTP,
+ }
+ args := []string{"smtp-tests"}
+ args = append(args, tc.args...)
+
+ err := app.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go
index ab769f6d0c..a5f1bd5105 100644
--- a/cmd/admin_regenerate.go
+++ b/cmd/admin_regenerate.go
@@ -4,11 +4,13 @@
package cmd
import (
+ "context"
+
"code.gitea.io/gitea/modules/graceful"
asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -25,20 +27,14 @@ var (
}
)
-func runRegenerateHooks(_ *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
}
-func runRegenerateKeys(_ *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user.go b/cmd/admin_user.go
index 967a6ed88a..3a24c3e56f 100644
--- a/cmd/admin_user.go
+++ b/cmd/admin_user.go
@@ -4,18 +4,18 @@
package cmd
import (
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var subcmdUser = &cli.Command{
Name: "user",
Usage: "Modify users",
- Subcommands: []*cli.Command{
- microcmdUserCreate,
+ Commands: []*cli.Command{
+ microcmdUserCreate(),
microcmdUserList,
- microcmdUserChangePassword,
- microcmdUserDelete,
+ microcmdUserChangePassword(),
+ microcmdUserDelete(),
microcmdUserGenerateAccessToken,
- microcmdUserMustChangePassword,
+ microcmdUserMustChangePassword(),
},
}
diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go
index f1ed46e70b..c27905b4db 100644
--- a/cmd/admin_user_change_password.go
+++ b/cmd/admin_user_change_password.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
@@ -13,44 +14,41 @@ import (
"code.gitea.io/gitea/modules/setting"
user_service "code.gitea.io/gitea/services/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserChangePassword = &cli.Command{
- Name: "change-password",
- Usage: "Change a user's password",
- Action: runChangePassword,
- Flags: []cli.Flag{
- &cli.StringFlag{
- Name: "username",
- Aliases: []string{"u"},
- Value: "",
- Usage: "The user to change password for",
+func microcmdUserChangePassword() *cli.Command {
+ return &cli.Command{
+ Name: "change-password",
+ Usage: "Change a user's password",
+ Action: runChangePassword,
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "username",
+ Aliases: []string{"u"},
+ Usage: "The user to change password for",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: "password",
+ Aliases: []string{"p"},
+ Usage: "New password to set for user",
+ Required: true,
+ },
+ &cli.BoolFlag{
+ Name: "must-change-password",
+ Usage: "User must change password (can be disabled by --must-change-password=false)",
+ Value: true,
+ },
},
- &cli.StringFlag{
- Name: "password",
- Aliases: []string{"p"},
- Value: "",
- Usage: "New password to set for user",
- },
- &cli.BoolFlag{
- Name: "must-change-password",
- Usage: "User must change password (can be disabled by --must-change-password=false)",
- Value: true,
- },
- },
+ }
}
-func runChangePassword(c *cli.Context) error {
- if err := argsSet(c, "username", "password"); err != nil {
- return err
- }
-
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
- return err
+func runChangePassword(ctx context.Context, c *cli.Command) error {
+ if !setting.IsInTesting {
+ if err := initDB(ctx); err != nil {
+ return err
+ }
}
user, err := user_model.GetUserByName(ctx, c.String("username"))
diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go
new file mode 100644
index 0000000000..17d0382af7
--- /dev/null
+++ b/cmd/admin_user_change_password_test.go
@@ -0,0 +1,91 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestChangePasswordCommand(t *testing.T) {
+ ctx := t.Context()
+
+ defer func() {
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ }()
+
+ t.Run("change password successfully", func(t *testing.T) {
+ // defer func() {
+ // require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ // }()
+ // Prepare test user
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+
+ // load test user
+ userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+
+ // Change the password
+ err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"})
+ require.NoError(t, err)
+
+ // Verify the password has been changed
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.NotEqual(t, userBase.Passwd, user.Passwd)
+ assert.NotEqual(t, userBase.Salt, user.Salt)
+
+ // Additional check for must-change-password flag
+ require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"}))
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, user.MustChangePassword)
+
+ require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"}))
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, user.MustChangePassword)
+ })
+
+ t.Run("failure cases", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ expectedErr string
+ }{
+ {
+ name: "user does not exist",
+ args: []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"},
+ expectedErr: "user does not exist",
+ },
+ {
+ name: "missing username",
+ args: []string{"change-password", "--password", "newpassword"},
+ expectedErr: `"username" not set`,
+ },
+ {
+ name: "missing password",
+ args: []string{"change-password", "--username", "testuser"},
+ expectedErr: `"password" not set`,
+ },
+ {
+ name: "too short password",
+ args: []string{"change-password", "--username", "testuser", "--password", "1"},
+ expectedErr: "password is not long enough",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := microcmdUserChangePassword().Run(ctx, tc.args)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.expectedErr)
+ })
+ }
+ })
+}
diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index 97f9bb7f06..cbdb5f90e2 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -16,87 +16,95 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserCreate = &cli.Command{
- Name: "create",
- Usage: "Create a new user in database",
- Action: runCreateUser,
- Flags: []cli.Flag{
- &cli.StringFlag{
- Name: "name",
- Usage: "Username. DEPRECATED: use username instead",
+func microcmdUserCreate() *cli.Command {
+ return &cli.Command{
+ Name: "create",
+ Usage: "Create a new user in database",
+ Action: runCreateUser,
+ MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
+ {
+ Flags: [][]cli.Flag{
+ {
+ &cli.StringFlag{
+ Name: "name",
+ Usage: "Username. DEPRECATED: use username instead",
+ },
+ &cli.StringFlag{
+ Name: "username",
+ Usage: "Username",
+ },
+ },
+ },
+ Required: true,
+ },
},
- &cli.StringFlag{
- Name: "username",
- Usage: "Username",
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "user-type",
+ Usage: "Set user's type: individual or bot",
+ Value: "individual",
+ },
+ &cli.StringFlag{
+ Name: "password",
+ Usage: "User password",
+ },
+ &cli.StringFlag{
+ Name: "email",
+ Usage: "User email address",
+ Required: true,
+ },
+ &cli.BoolFlag{
+ Name: "admin",
+ Usage: "User is an admin",
+ },
+ &cli.BoolFlag{
+ Name: "random-password",
+ Usage: "Generate a random password for the user",
+ },
+ &cli.BoolFlag{
+ Name: "must-change-password",
+ Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
+ HideDefault: true,
+ },
+ &cli.IntFlag{
+ Name: "random-password-length",
+ Usage: "Length of the random password to be generated",
+ Value: 12,
+ },
+ &cli.BoolFlag{
+ Name: "access-token",
+ Usage: "Generate access token for the user",
+ },
+ &cli.StringFlag{
+ Name: "access-token-name",
+ Usage: `Name of the generated access token`,
+ Value: "gitea-admin",
+ },
+ &cli.StringFlag{
+ Name: "access-token-scopes",
+ Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
+ Value: "all",
+ },
+ &cli.BoolFlag{
+ Name: "restricted",
+ Usage: "Make a restricted user account",
+ },
+ &cli.StringFlag{
+ Name: "fullname",
+ Usage: `The full, human-readable name of the user`,
+ },
},
- &cli.StringFlag{
- Name: "user-type",
- Usage: "Set user's type: individual or bot",
- Value: "individual",
- },
- &cli.StringFlag{
- Name: "password",
- Usage: "User password",
- },
- &cli.StringFlag{
- Name: "email",
- Usage: "User email address",
- },
- &cli.BoolFlag{
- Name: "admin",
- Usage: "User is an admin",
- },
- &cli.BoolFlag{
- Name: "random-password",
- Usage: "Generate a random password for the user",
- },
- &cli.BoolFlag{
- Name: "must-change-password",
- Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
- DisableDefaultText: true,
- },
- &cli.IntFlag{
- Name: "random-password-length",
- Usage: "Length of the random password to be generated",
- Value: 12,
- },
- &cli.BoolFlag{
- Name: "access-token",
- Usage: "Generate access token for the user",
- },
- &cli.StringFlag{
- Name: "access-token-name",
- Usage: `Name of the generated access token`,
- Value: "gitea-admin",
- },
- &cli.StringFlag{
- Name: "access-token-scopes",
- Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
- Value: "all",
- },
- &cli.BoolFlag{
- Name: "restricted",
- Usage: "Make a restricted user account",
- },
- &cli.StringFlag{
- Name: "fullname",
- Usage: `The full, human-readable name of the user`,
- },
- },
+ }
}
-func runCreateUser(c *cli.Context) error {
+func runCreateUser(ctx context.Context, c *cli.Command) error {
// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings()
- if err := argsSet(c, "email"); err != nil {
- return err
- }
-
userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual,
"bot": user_model.UserTypeBot,
@@ -113,12 +121,6 @@ func runCreateUser(c *cli.Context) error {
return errors.New("password can only be set for individual users")
}
}
- if c.IsSet("name") && c.IsSet("username") {
- return errors.New("cannot set both --name and --username flags")
- }
- if !c.IsSet("name") && !c.IsSet("username") {
- return errors.New("one of --name or --username flags must be set")
- }
if c.IsSet("password") && c.IsSet("random-password") {
return errors.New("cannot set both -random-password and -password flags")
@@ -129,16 +131,12 @@ func runCreateUser(c *cli.Context) error {
username = c.String("username")
} else {
username = c.String("name")
- _, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
+ _, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
}
- ctx := c.Context
if !setting.IsInTesting {
- // FIXME: need to refactor the "installSignals/initDB" related code later
+ // FIXME: need to refactor the "initDB" related code later
// it doesn't make sense to call it in (almost) every command action function
- var cancel context.CancelFunc
- ctx, cancel = installSignals()
- defer cancel()
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go
index d5952412c3..437e07d9a2 100644
--- a/cmd/admin_user_create_test.go
+++ b/cmd/admin_user_create_test.go
@@ -18,8 +18,6 @@ import (
)
func TestAdminUserCreate(t *testing.T) {
- app := NewMainApp(AppVersion{})
-
reset := func() {
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
@@ -31,8 +29,9 @@ func TestAdminUserCreate(t *testing.T) {
IsAdmin bool
MustChangePassword bool
}
+
createCheck := func(name, args string) check {
- require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
+ require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
}
@@ -51,7 +50,7 @@ func TestAdminUserCreate(t *testing.T) {
})
createUser := func(name string, args ...string) error {
- return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...))
+ return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...))
}
t.Run("UserType", func(t *testing.T) {
diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go
index 520557554a..f91041577c 100644
--- a/cmd/admin_user_delete.go
+++ b/cmd/admin_user_delete.go
@@ -4,53 +4,56 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"strings"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
user_service "code.gitea.io/gitea/services/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserDelete = &cli.Command{
- Name: "delete",
- Usage: "Delete specific user by id, name or email",
- Flags: []cli.Flag{
- &cli.Int64Flag{
- Name: "id",
- Usage: "ID of user of the user to delete",
+func microcmdUserDelete() *cli.Command {
+ return &cli.Command{
+ Name: "delete",
+ Usage: "Delete specific user by id, name or email",
+ Flags: []cli.Flag{
+ &cli.Int64Flag{
+ Name: "id",
+ Usage: "ID of user of the user to delete",
+ },
+ &cli.StringFlag{
+ Name: "username",
+ Aliases: []string{"u"},
+ Usage: "Username of the user to delete",
+ },
+ &cli.StringFlag{
+ Name: "email",
+ Aliases: []string{"e"},
+ Usage: "Email of the user to delete",
+ },
+ &cli.BoolFlag{
+ Name: "purge",
+ Usage: "Purge user, all their repositories, organizations and comments",
+ },
},
- &cli.StringFlag{
- Name: "username",
- Aliases: []string{"u"},
- Usage: "Username of the user to delete",
- },
- &cli.StringFlag{
- Name: "email",
- Aliases: []string{"e"},
- Usage: "Email of the user to delete",
- },
- &cli.BoolFlag{
- Name: "purge",
- Usage: "Purge user, all their repositories, organizations and comments",
- },
- },
- Action: runDeleteUser,
+ Action: runDeleteUser,
+ }
}
-func runDeleteUser(c *cli.Context) error {
+func runDeleteUser(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
return errors.New("You must provide the id, username or email of a user to delete")
}
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
- return err
+ if !setting.IsInTesting {
+ if err := initDB(ctx); err != nil {
+ return err
+ }
}
if err := storage.Init(); err != nil {
@@ -70,11 +73,11 @@ func runDeleteUser(c *cli.Context) error {
return err
}
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
- return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
+ return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
}
if c.IsSet("id") && user.ID != c.Int64("id") {
- return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
+ return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id"))
}
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
diff --git a/cmd/admin_user_delete_test.go b/cmd/admin_user_delete_test.go
new file mode 100644
index 0000000000..d0330582d7
--- /dev/null
+++ b/cmd/admin_user_delete_test.go
@@ -0,0 +1,111 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "strconv"
+ "strings"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAdminUserDelete(t *testing.T) {
+ ctx := t.Context()
+ defer func() {
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
+ }()
+
+ setupTestUser := func(t *testing.T) {
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ }
+
+ t.Run("delete user by id", func(t *testing.T) {
+ setupTestUser(t)
+
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+ t.Run("delete user by username", func(t *testing.T) {
+ setupTestUser(t)
+
+ err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+ t.Run("delete user by email", func(t *testing.T) {
+ setupTestUser(t)
+
+ err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+ t.Run("delete user by all 3 attributes", func(t *testing.T) {
+ setupTestUser(t)
+
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+}
+
+func TestAdminUserDeleteFailure(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ expectedErr string
+ }{
+ {
+ name: "no user to delete",
+ args: []string{"delete", "--username", "nonexistentuser"},
+ expectedErr: "user does not exist",
+ },
+ {
+ name: "user exists but provided username does not match",
+ args: []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"},
+ expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername",
+ },
+ {
+ name: "user exists but provided id does not match",
+ args: []string{"delete", "--username", "testuser", "--id", "999"},
+ expectedErr: "the user testuser does not match the provided id 999",
+ },
+ {
+ name: "no required flags are provided",
+ args: []string{"delete"},
+ expectedErr: "You must provide the id, username or email of a user to delete",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctx := t.Context()
+ if strings.Contains(tc.name, "user exists") {
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ }
+
+ err := microcmdUserDelete().Run(ctx, tc.args)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.expectedErr)
+ })
+
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
+ }
+}
diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go
index f6db7a74bd..61064fdef4 100644
--- a/cmd/admin_user_generate_access_token.go
+++ b/cmd/admin_user_generate_access_token.go
@@ -4,13 +4,14 @@
package cmd
import (
+ "context"
"errors"
"fmt"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var microcmdUserGenerateAccessToken = &cli.Command{
@@ -41,14 +42,11 @@ var microcmdUserGenerateAccessToken = &cli.Command{
Action: runGenerateAccessToken,
}
-func runGenerateAccessToken(c *cli.Context) error {
+func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") {
return errors.New("you must provide a username to generate a token for")
}
- ctx, cancel := installSignals()
- defer cancel()
-
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go
index 4c2b26d1df..e3d345e2f2 100644
--- a/cmd/admin_user_list.go
+++ b/cmd/admin_user_list.go
@@ -4,13 +4,14 @@
package cmd
import (
+ "context"
"fmt"
"os"
"text/tabwriter"
user_model "code.gitea.io/gitea/models/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var microcmdUserList = &cli.Command{
@@ -25,10 +26,7 @@ var microcmdUserList = &cli.Command{
},
}
-func runListUsers(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runListUsers(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go
index 2794414259..8521853dc1 100644
--- a/cmd/admin_user_must_change_password.go
+++ b/cmd/admin_user_must_change_password.go
@@ -4,40 +4,41 @@
package cmd
import (
+ "context"
"errors"
"fmt"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserMustChangePassword = &cli.Command{
- Name: "must-change-password",
- Usage: "Set the must change password flag for the provided users or all users",
- Action: runMustChangePassword,
- Flags: []cli.Flag{
- &cli.BoolFlag{
- Name: "all",
- Aliases: []string{"A"},
- Usage: "All users must change password, except those explicitly excluded with --exclude",
+func microcmdUserMustChangePassword() *cli.Command {
+ return &cli.Command{
+ Name: "must-change-password",
+ Usage: "Set the must change password flag for the provided users or all users",
+ Action: runMustChangePassword,
+ Flags: []cli.Flag{
+ &cli.BoolFlag{
+ Name: "all",
+ Aliases: []string{"A"},
+ Usage: "All users must change password, except those explicitly excluded with --exclude",
+ },
+ &cli.StringSliceFlag{
+ Name: "exclude",
+ Aliases: []string{"e"},
+ Usage: "Do not change the must-change-password flag for these users",
+ },
+ &cli.BoolFlag{
+ Name: "unset",
+ Usage: "Instead of setting the must-change-password flag, unset it",
+ },
},
- &cli.StringSliceFlag{
- Name: "exclude",
- Aliases: []string{"e"},
- Usage: "Do not change the must-change-password flag for these users",
- },
- &cli.BoolFlag{
- Name: "unset",
- Usage: "Instead of setting the must-change-password flag, unset it",
- },
- },
+ }
}
-func runMustChangePassword(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runMustChangePassword(ctx context.Context, c *cli.Command) error {
if c.NArg() == 0 && !c.IsSet("all") {
return errors.New("either usernames or --all must be provided")
}
@@ -46,8 +47,10 @@ func runMustChangePassword(c *cli.Context) error {
all := c.Bool("all")
exclude := c.StringSlice("exclude")
- if err := initDB(ctx); err != nil {
- return err
+ if !setting.IsInTesting {
+ if err := initDB(ctx); err != nil {
+ return err
+ }
}
n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)
diff --git a/cmd/admin_user_must_change_password_test.go b/cmd/admin_user_must_change_password_test.go
new file mode 100644
index 0000000000..a6611fdc04
--- /dev/null
+++ b/cmd/admin_user_must_change_password_test.go
@@ -0,0 +1,78 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMustChangePassword(t *testing.T) {
+ defer func() {
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ }()
+ err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ // Reset password change flag
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
+ require.NoError(t, err)
+
+ testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, testUser.MustChangePassword)
+ testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+
+ // Make all users change password
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.True(t, testUserExclude.MustChangePassword)
+
+ // Reset password change flag but exclude all tested users
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.True(t, testUserExclude.MustChangePassword)
+
+ // Reset password change flag by listing multiple users
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+
+ // Exclude a user from all user
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+
+ // Unset a flag for single user
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+}
diff --git a/cmd/cert.go b/cmd/cert.go
index 38241d71a3..8cc9f43528 100644
--- a/cmd/cert.go
+++ b/cmd/cert.go
@@ -6,6 +6,7 @@
package cmd
import (
+ "context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@@ -13,6 +14,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
+ "fmt"
"log"
"math/big"
"net"
@@ -20,47 +22,59 @@ import (
"strings"
"time"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-// CmdCert represents the available cert sub-command.
-var CmdCert = &cli.Command{
- Name: "cert",
- Usage: "Generate self-signed certificate",
- Description: `Generate a self-signed X.509 certificate for a TLS server.
+// cmdCert represents the available cert sub-command.
+func cmdCert() *cli.Command {
+ return &cli.Command{
+ Name: "cert",
+ Usage: "Generate self-signed certificate",
+ Description: `Generate a self-signed X.509 certificate for a TLS server.
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
- Action: runCert,
- Flags: []cli.Flag{
- &cli.StringFlag{
- Name: "host",
- Value: "",
- Usage: "Comma-separated hostnames and IPs to generate a certificate for",
+ Action: runCert,
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "host",
+ Usage: "Comma-separated hostnames and IPs to generate a certificate for",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: "ecdsa-curve",
+ Value: "",
+ Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
+ },
+ &cli.IntFlag{
+ Name: "rsa-bits",
+ Value: 3072,
+ Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
+ },
+ &cli.StringFlag{
+ Name: "start-date",
+ Value: "",
+ Usage: "Creation date formatted as Jan 1 15:04:05 2011",
+ },
+ &cli.DurationFlag{
+ Name: "duration",
+ Value: 365 * 24 * time.Hour,
+ Usage: "Duration that certificate is valid for",
+ },
+ &cli.BoolFlag{
+ Name: "ca",
+ Usage: "whether this cert should be its own Certificate Authority",
+ },
+ &cli.StringFlag{
+ Name: "out",
+ Value: "cert.pem",
+ Usage: "Path to the file where there certificate will be saved",
+ },
+ &cli.StringFlag{
+ Name: "keyout",
+ Value: "key.pem",
+ Usage: "Path to the file where there certificate key will be saved",
+ },
},
- &cli.StringFlag{
- Name: "ecdsa-curve",
- Value: "",
- Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
- },
- &cli.IntFlag{
- Name: "rsa-bits",
- Value: 3072,
- Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
- },
- &cli.StringFlag{
- Name: "start-date",
- Value: "",
- Usage: "Creation date formatted as Jan 1 15:04:05 2011",
- },
- &cli.DurationFlag{
- Name: "duration",
- Value: 365 * 24 * time.Hour,
- Usage: "Duration that certificate is valid for",
- },
- &cli.BoolFlag{
- Name: "ca",
- Usage: "whether this cert should be its own Certificate Authority",
- },
- },
+ }
}
func publicKey(priv any) any {
@@ -89,11 +103,7 @@ func pemBlockForKey(priv any) *pem.Block {
}
}
-func runCert(c *cli.Context) error {
- if err := argsSet(c, "host"); err != nil {
- return err
- }
-
+func runCert(_ context.Context, c *cli.Command) error {
var priv any
var err error
switch c.String("ecdsa-curve") {
@@ -108,17 +118,17 @@ func runCert(c *cli.Context) error {
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
- log.Fatalf("Unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
+ err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
}
if err != nil {
- log.Fatalf("Failed to generate private key: %v", err)
+ return fmt.Errorf("failed to generate private key: %w", err)
}
var notBefore time.Time
if startDate := c.String("start-date"); startDate != "" {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
if err != nil {
- log.Fatalf("Failed to parse creation date: %v", err)
+ return fmt.Errorf("failed to parse creation date %w", err)
}
} else {
notBefore = time.Now()
@@ -129,7 +139,7 @@ func runCert(c *cli.Context) error {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
- log.Fatalf("Failed to generate serial number: %v", err)
+ return fmt.Errorf("failed to generate serial number: %w", err)
}
template := x509.Certificate{
@@ -162,35 +172,35 @@ func runCert(c *cli.Context) error {
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
- log.Fatalf("Failed to create certificate: %v", err)
+ return fmt.Errorf("failed to create certificate: %w", err)
}
- certOut, err := os.Create("cert.pem")
+ certOut, err := os.Create(c.String("out"))
if err != nil {
- log.Fatalf("Failed to open cert.pem for writing: %v", err)
+ return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
}
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
- log.Fatalf("Failed to encode certificate: %v", err)
+ return fmt.Errorf("failed to encode certificate: %w", err)
}
err = certOut.Close()
if err != nil {
- log.Fatalf("Failed to write cert: %v", err)
+ return fmt.Errorf("failed to write cert: %w", err)
}
- log.Println("Written cert.pem")
+ fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out"))
- keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
+ keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
- log.Fatalf("Failed to open key.pem for writing: %v", err)
+ return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
}
err = pem.Encode(keyOut, pemBlockForKey(priv))
if err != nil {
- log.Fatalf("Failed to encode key: %v", err)
+ return fmt.Errorf("failed to encode key: %w", err)
}
err = keyOut.Close()
if err != nil {
- log.Fatalf("Failed to write key: %v", err)
+ return fmt.Errorf("failed to write key: %w", err)
}
- log.Println("Written key.pem")
+ fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout"))
return nil
}
diff --git a/cmd/cert_test.go b/cmd/cert_test.go
new file mode 100644
index 0000000000..4242d8915b
--- /dev/null
+++ b/cmd/cert_test.go
@@ -0,0 +1,123 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCertCommand(t *testing.T) {
+ cases := []struct {
+ name string
+ args []string
+ }{
+ {
+ name: "RSA cert generation",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--rsa-bits", "2048",
+ "--duration", "1h",
+ "--start-date", "Jan 1 00:00:00 2024",
+ },
+ },
+ {
+ name: "ECDSA cert generation",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--ecdsa-curve", "P256",
+ "--duration", "1h",
+ "--start-date", "Jan 1 00:00:00 2024",
+ },
+ },
+ {
+ name: "mixed host, certificate authority",
+ args: []string{
+ "cert-test",
+ "--host", "localhost,127.0.0.1",
+ "--duration", "1h",
+ "--start-date", "Jan 1 00:00:00 2024",
+ },
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ app := cmdCert()
+ tempDir := t.TempDir()
+
+ certFile := filepath.Join(tempDir, "cert.pem")
+ keyFile := filepath.Join(tempDir, "key.pem")
+
+ err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
+ require.NoError(t, err)
+
+ assert.FileExists(t, certFile)
+ assert.FileExists(t, keyFile)
+ })
+ }
+}
+
+func TestCertCommandFailures(t *testing.T) {
+ cases := []struct {
+ name string
+ args []string
+ errMsg string
+ }{
+ {
+ name: "Start Date Parsing failure",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--start-date", "invalid-date",
+ },
+ errMsg: "parsing time",
+ },
+ {
+ name: "Unknown curve",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--ecdsa-curve", "invalid-curve",
+ },
+ errMsg: "unrecognized elliptic curve",
+ },
+ {
+ name: "Key generation failure",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--rsa-bits", "invalid-bits",
+ },
+ },
+ {
+ name: "Missing parameters",
+ args: []string{
+ "cert-test",
+ },
+ errMsg: `"host" not set`,
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ app := cmdCert()
+ tempDir := t.TempDir()
+
+ certFile := filepath.Join(tempDir, "cert.pem")
+ keyFile := filepath.Join(tempDir, "key.pem")
+ err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
+ require.Error(t, err)
+ if c.errMsg != "" {
+ assert.ErrorContains(t, err, c.errMsg)
+ }
+ assert.NoFileExists(t, certFile)
+ assert.NoFileExists(t, keyFile)
+ })
+ }
+}
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 423dce2674..7a4d5d0d89 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -18,20 +18,19 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// argsSet checks that all the required arguments are set. args is a list of
// arguments that must be set in the passed Context.
-func argsSet(c *cli.Context, args ...string) error {
+func argsSet(c *cli.Command, args ...string) error {
for _, a := range args {
if !c.IsSet(a) {
return errors.New(a + " is not set")
}
- if util.IsEmptyString(c.String(a)) {
+ if c.Value(a) == nil {
return errors.New(a + " is required")
}
}
@@ -109,7 +108,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
}
-func globalBool(c *cli.Context, name string) bool {
+func globalBool(c *cli.Command, name string) bool {
for _, ctx := range c.Lineage() {
if ctx.Bool(name) {
return true
@@ -120,8 +119,8 @@ func globalBool(c *cli.Context, name string) bool {
// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout.
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
-func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error {
- return func(c *cli.Context) error {
+func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
+ return func(ctx context.Context, c *cli.Command) (context.Context, error) {
level := defaultLevel
if globalBool(c, "quiet") {
level = log.FATAL
@@ -130,6 +129,6 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error
level = log.TRACE
}
log.SetConsoleLogger(log.DEFAULT, "console-default", level)
- return nil
+ return ctx, nil
}
}
diff --git a/cmd/docs.go b/cmd/docs.go
index 605d02e3ef..098c0e9a8a 100644
--- a/cmd/docs.go
+++ b/cmd/docs.go
@@ -4,11 +4,13 @@
package cmd
import (
+ "context"
"fmt"
"os"
"strings"
- "github.com/urfave/cli/v2"
+ cli_docs "github.com/urfave/cli-docs/v3"
+ "github.com/urfave/cli/v3"
)
// CmdDocs represents the available docs sub-command.
@@ -30,16 +32,16 @@ var CmdDocs = &cli.Command{
},
}
-func runDocs(ctx *cli.Context) error {
- docs, err := ctx.App.ToMarkdown()
- if ctx.Bool("man") {
- docs, err = ctx.App.ToMan()
+func runDocs(_ context.Context, cmd *cli.Command) error {
+ docs, err := cli_docs.ToMarkdown(cmd.Root())
+ if cmd.Bool("man") {
+ docs, err = cli_docs.ToMan(cmd.Root())
}
if err != nil {
return err
}
- if !ctx.Bool("man") {
+ if !cmd.Bool("man") {
// Clean up markdown. The following bug was fixed in v2, but is present in v1.
// It affects markdown output (even though the issue is referring to man pages)
// https://github.com/urfave/cli/issues/1040
@@ -51,8 +53,8 @@ func runDocs(ctx *cli.Context) error {
}
out := os.Stdout
- if ctx.String("output") != "" {
- fi, err := os.Create(ctx.String("output"))
+ if cmd.String("output") != "" {
+ fi, err := os.Create(cmd.String("output"))
if err != nil {
return err
}
diff --git a/cmd/doctor.go b/cmd/doctor.go
index 4a12b957f5..9e0fcbf877 100644
--- a/cmd/doctor.go
+++ b/cmd/doctor.go
@@ -20,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/doctor"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
"xorm.io/xorm"
)
@@ -30,7 +30,7 @@ var CmdDoctor = &cli.Command{
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
cmdDoctorCheck,
cmdRecreateTable,
cmdDoctorConvert,
@@ -93,16 +93,13 @@ You should back-up your database before doing this and ensure that your database
Action: runRecreateTable,
}
-func runRecreateTable(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
+func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
// Redirect the default golog to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
- debug := ctx.Bool("debug")
+ debug := cmd.Bool("debug")
setting.MustInstalled()
setting.LoadDBSetting()
@@ -113,15 +110,15 @@ func runRecreateTable(ctx *cli.Context) error {
}
setting.Database.LogSQL = debug
- if err := db.InitEngine(stdCtx); err != nil {
+ if err := db.InitEngine(ctx); err != nil {
fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil
}
- args := ctx.Args()
- names := make([]string, 0, ctx.NArg())
- for i := 0; i < ctx.NArg(); i++ {
+ args := cmd.Args()
+ names := make([]string, 0, cmd.NArg())
+ for i := 0; i < cmd.NArg(); i++ {
names = append(names, args.Get(i))
}
@@ -131,7 +128,7 @@ func runRecreateTable(ctx *cli.Context) error {
}
recreateTables := migrate_base.RecreateTables(beans...)
- return db.InitEngineWithMigration(stdCtx, func(ctx context.Context, x *xorm.Engine) error {
+ return db.InitEngineWithMigration(ctx, func(ctx context.Context, x *xorm.Engine) error {
if err := migrations.EnsureUpToDate(ctx, x); err != nil {
return err
}
@@ -139,11 +136,11 @@ func runRecreateTable(ctx *cli.Context) error {
})
}
-func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
+func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
// Silence the default loggers
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
- logFile := ctx.String("log-file")
+ logFile := cmd.String("log-file")
switch logFile {
case "":
return // if no doctor log-file is set, do not show any log from default logger
@@ -161,23 +158,20 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
}
}
-func runDoctorCheck(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
+func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
colorize := log.CanColorStdout
- if ctx.IsSet("color") {
- colorize = ctx.Bool("color")
+ if cmd.IsSet("color") {
+ colorize = cmd.Bool("color")
}
- setupDoctorDefaultLogger(ctx, colorize)
+ setupDoctorDefaultLogger(cmd, colorize)
// Finally redirect the default golang's log to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
- if ctx.IsSet("list") {
+ if cmd.IsSet("list") {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
doctor.SortChecks(doctor.Checks)
@@ -195,12 +189,12 @@ func runDoctorCheck(ctx *cli.Context) error {
}
var checks []*doctor.Check
- if ctx.Bool("all") {
+ if cmd.Bool("all") {
checks = make([]*doctor.Check, len(doctor.Checks))
copy(checks, doctor.Checks)
- } else if ctx.IsSet("run") {
- addDefault := ctx.Bool("default")
- runNamesSet := container.SetOf(ctx.StringSlice("run")...)
+ } else if cmd.IsSet("run") {
+ addDefault := cmd.Bool("default")
+ runNamesSet := container.SetOf(cmd.StringSlice("run")...)
for _, check := range doctor.Checks {
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
checks = append(checks, check)
@@ -217,5 +211,5 @@ func runDoctorCheck(ctx *cli.Context) error {
}
}
}
- return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
+ return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks)
}
diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go
index 48c835ad0e..8cb718d383 100644
--- a/cmd/doctor_convert.go
+++ b/cmd/doctor_convert.go
@@ -4,13 +4,14 @@
package cmd
import (
+ "context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// cmdDoctorConvert represents the available convert sub-command.
@@ -21,11 +22,8 @@ var cmdDoctorConvert = &cli.Command{
Action: runDoctorConvert,
}
-func runDoctorConvert(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
+ if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go
index 3e1ff299c5..da942b38b6 100644
--- a/cmd/doctor_test.go
+++ b/cmd/doctor_test.go
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/services/doctor"
"github.com/stretchr/testify/assert"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func TestDoctorRun(t *testing.T) {
@@ -22,12 +22,13 @@ func TestDoctorRun(t *testing.T) {
SkipDatabaseInitialization: true,
})
- app := cli.NewApp()
- app.Commands = []*cli.Command{cmdDoctorCheck}
- err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
+ app := &cli.Command{
+ Commands: []*cli.Command{cmdDoctorCheck},
+ }
+ err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
assert.NoError(t, err)
- err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
+ err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
- err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"})
+ err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
}
diff --git a/cmd/dump.go b/cmd/dump.go
index 7d640b78fd..ed19e3d4bf 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -5,6 +5,7 @@
package cmd
import (
+ "context"
"os"
"path"
"path/filepath"
@@ -20,7 +21,7 @@ import (
"gitea.com/go-chi/session"
"github.com/mholt/archiver/v3"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdDump represents the available dump sub-command.
@@ -101,17 +102,17 @@ func fatal(format string, args ...any) {
log.Fatal(format, args...)
}
-func runDump(ctx *cli.Context) error {
+func runDump(ctx context.Context, cmd *cli.Command) error {
setting.MustInstalled()
- quite := ctx.Bool("quiet")
- verbose := ctx.Bool("verbose")
+ quite := cmd.Bool("quiet")
+ verbose := cmd.Bool("verbose")
if verbose && quite {
fatal("Option --quiet and --verbose cannot both be set")
}
// outFileName is either "-" or a file name (will be made absolute)
- outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
+ outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type"))
if outType == "" {
fatal("Invalid output type")
}
@@ -136,10 +137,7 @@ func runDump(ctx *cli.Context) error {
setting.DisableLoggerInit()
setting.LoadSettings() // cannot access session settings otherwise
- stdCtx, cancel := installSignals()
- defer cancel()
-
- err := db.InitEngine(stdCtx)
+ err := db.InitEngine(ctx)
if err != nil {
return err
}
@@ -165,7 +163,7 @@ func runDump(ctx *cli.Context) error {
}
dumper.GlobalExcludeAbsPath(outFileName)
- if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
+ if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") {
log.Info("Skip dumping local repositories")
} else {
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
@@ -173,7 +171,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to include repositories: %v", err)
}
- if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
+ if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") {
log.Info("Skip dumping LFS data")
} else if !setting.LFS.StartServer {
log.Info("LFS isn't enabled. Skip dumping LFS data")
@@ -188,12 +186,12 @@ func runDump(ctx *cli.Context) error {
}
}
- if ctx.Bool("skip-db") {
+ if cmd.Bool("skip-db") {
// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
dumper.GlobalExcludeAbsPath(setting.Database.Path)
log.Info("Skipping database")
} else {
- tmpDir := ctx.String("tempdir")
+ tmpDir := cmd.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
fatal("Path does not exist: %s", tmpDir)
}
@@ -209,7 +207,7 @@ func runDump(ctx *cli.Context) error {
}
}()
- targetDBType := ctx.String("database")
+ targetDBType := cmd.String("database")
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
} else {
@@ -230,7 +228,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to include specified app.ini: %v", err)
}
- if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
+ if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") {
log.Info("Skipping custom directory")
} else {
customDir, err := os.Stat(setting.CustomPath)
@@ -263,7 +261,7 @@ func runDump(ctx *cli.Context) error {
excludes = append(excludes, opts.ProviderConfig)
}
- if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
+ if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
excludes = append(excludes, setting.Indexer.RepoPath)
excludes = append(excludes, setting.Indexer.IssuePath)
}
@@ -278,7 +276,7 @@ func runDump(ctx *cli.Context) error {
}
}
- if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
+ if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") {
log.Info("Skip dumping attachment data")
} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
@@ -290,7 +288,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to dump attachments: %v", err)
}
- if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
+ if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") {
log.Info("Skip dumping package data")
} else if !setting.Packages.Enabled {
log.Info("Packages isn't enabled. Skip dumping package data")
@@ -307,7 +305,7 @@ func runDump(ctx *cli.Context) error {
// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not.
- if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
+ if cmd.IsSet("skip-log") && cmd.Bool("skip-log") {
log.Info("Skip dumping log files")
} else {
isExist, err := util.IsExist(setting.Log.RootPath)
diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go
index 11d0270404..8dd4fd86e7 100644
--- a/cmd/dump_repo.go
+++ b/cmd/dump_repo.go
@@ -19,7 +19,7 @@ import (
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdDumpRepository represents the available dump repository sub-command.
@@ -79,16 +79,13 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
},
}
-func runDumpRepository(ctx *cli.Context) error {
+func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
setting.DisableLoggerInit()
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+ if err := initDB(ctx); err != nil {
return err
}
@@ -105,8 +102,8 @@ func runDumpRepository(ctx *cli.Context) error {
var (
serviceType structs.GitServiceType
- cloneAddr = ctx.String("clone_addr")
- serviceStr = ctx.String("git_service")
+ cloneAddr = cmd.String("clone_addr")
+ serviceStr = cmd.String("git_service")
)
if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") {
@@ -124,13 +121,13 @@ func runDumpRepository(ctx *cli.Context) error {
opts := base.MigrateOptions{
GitServiceType: serviceType,
CloneAddr: cloneAddr,
- AuthUsername: ctx.String("auth_username"),
- AuthPassword: ctx.String("auth_password"),
- AuthToken: ctx.String("auth_token"),
- RepoName: ctx.String("repo_name"),
+ AuthUsername: cmd.String("auth_username"),
+ AuthPassword: cmd.String("auth_password"),
+ AuthToken: cmd.String("auth_token"),
+ RepoName: cmd.String("repo_name"),
}
- if len(ctx.String("units")) == 0 {
+ if len(cmd.String("units")) == 0 {
opts.Wiki = true
opts.Issues = true
opts.Milestones = true
@@ -140,7 +137,7 @@ func runDumpRepository(ctx *cli.Context) error {
opts.PullRequests = true
opts.ReleaseAssets = true
} else {
- units := strings.Split(ctx.String("units"), ",")
+ units := strings.Split(cmd.String("units"), ",")
for _, unit := range units {
switch strings.ToLower(strings.TrimSpace(unit)) {
case "":
@@ -169,7 +166,7 @@ func runDumpRepository(ctx *cli.Context) error {
// the repo_dir will be removed if error occurs in DumpRepository
// make sure the directory doesn't exist or is empty, prevent from deleting user files
- repoDir := ctx.String("repo_dir")
+ repoDir := cmd.String("repo_dir")
if exists, err := util.IsExist(repoDir); err != nil {
return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
} else if exists {
@@ -184,7 +181,7 @@ func runDumpRepository(ctx *cli.Context) error {
if err := migrations.DumpRepository(
context.Background(),
repoDir,
- ctx.String("owner_name"),
+ cmd.String("owner_name"),
opts,
); err != nil {
log.Fatal("Failed to dump repository: %v", err)
diff --git a/cmd/embedded.go b/cmd/embedded.go
index 9f03f7be7c..086bc06863 100644
--- a/cmd/embedded.go
+++ b/cmd/embedded.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"os"
@@ -19,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdEmbedded represents the available extract sub-command.
@@ -28,7 +29,7 @@ var (
Name: "embedded",
Usage: "Extract embedded resources",
Description: "A command for extracting embedded resources, like templates and images",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdList,
subcmdView,
subcmdExtract,
@@ -100,7 +101,7 @@ type assetFile struct {
path string
}
-func initEmbeddedExtractor(c *cli.Context) error {
+func initEmbeddedExtractor(c *cli.Command) error {
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
patterns, err := compileCollectPatterns(c.Args().Slice())
@@ -115,7 +116,7 @@ func initEmbeddedExtractor(c *cli.Context) error {
return nil
}
-func runList(c *cli.Context) error {
+func runList(_ context.Context, c *cli.Command) error {
if err := runListDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
@@ -123,7 +124,7 @@ func runList(c *cli.Context) error {
return nil
}
-func runView(c *cli.Context) error {
+func runView(_ context.Context, c *cli.Command) error {
if err := runViewDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
@@ -131,7 +132,7 @@ func runView(c *cli.Context) error {
return nil
}
-func runExtract(c *cli.Context) error {
+func runExtract(_ context.Context, c *cli.Command) error {
if err := runExtractDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
@@ -139,7 +140,7 @@ func runExtract(c *cli.Context) error {
return nil
}
-func runListDo(c *cli.Context) error {
+func runListDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -151,7 +152,7 @@ func runListDo(c *cli.Context) error {
return nil
}
-func runViewDo(c *cli.Context) error {
+func runViewDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -174,7 +175,7 @@ func runViewDo(c *cli.Context) error {
return nil
}
-func runExtractDo(c *cli.Context) error {
+func runExtractDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -271,7 +272,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error {
return nil
}
-func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) {
+func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) {
fs := assetfs.Layered(layer)
files, err := fs.ListAllFiles(".", true)
if err != nil {
diff --git a/cmd/generate.go b/cmd/generate.go
index 90b32ecaf0..cf491604ef 100644
--- a/cmd/generate.go
+++ b/cmd/generate.go
@@ -5,13 +5,14 @@
package cmd
import (
+ "context"
"fmt"
"os"
"code.gitea.io/gitea/modules/generate"
"github.com/mattn/go-isatty"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -19,7 +20,7 @@ var (
CmdGenerate = &cli.Command{
Name: "generate",
Usage: "Generate Gitea's secrets/keys/tokens",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdSecret,
},
}
@@ -27,7 +28,7 @@ var (
subcmdSecret = &cli.Command{
Name: "secret",
Usage: "Generate a secret token",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
microcmdGenerateInternalToken,
microcmdGenerateLfsJwtSecret,
microcmdGenerateSecretKey,
@@ -54,7 +55,7 @@ var (
}
)
-func runGenerateInternalToken(c *cli.Context) error {
+func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
internalToken, err := generate.NewInternalToken()
if err != nil {
return err
@@ -69,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
return nil
}
-func runGenerateLfsJwtSecret(c *cli.Context) error {
+func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
if err != nil {
return err
@@ -84,7 +85,7 @@ func runGenerateLfsJwtSecret(c *cli.Context) error {
return nil
}
-func runGenerateSecretKey(c *cli.Context) error {
+func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
secretKey, err := generate.NewSecretKey()
if err != nil {
return err
diff --git a/cmd/hook.go b/cmd/hook.go
index 6f0aa5a203..4621137e01 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -20,7 +20,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
const (
@@ -34,7 +34,7 @@ var (
Usage: "(internal) Should only be called by Git",
Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL),
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdHookPreReceive,
subcmdHookUpdate,
subcmdHookPostReceive,
@@ -161,12 +161,10 @@ func (n *nilWriter) WriteString(s string) (int, error) {
return len(s), nil
}
-func runHookPreReceive(c *cli.Context) error {
+func runHookPreReceive(ctx context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
- ctx, cancel := installSignals()
- defer cancel()
setup(ctx, c.Bool("debug"))
@@ -292,7 +290,7 @@ Gitea or set your environment appropriately.`, "")
// runHookUpdate avoid to do heavy operations on update hook because it will be
// invoked for every ref update which does not like pre-receive and post-receive
-func runHookUpdate(c *cli.Context) error {
+func runHookUpdate(_ context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
@@ -309,15 +307,12 @@ func runHookUpdate(c *cli.Context) error {
return nil
}
-func runHookPostReceive(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runHookPostReceive(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
// First of all run update-server-info no matter what
if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil {
- return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
+ return fmt.Errorf("failed to call 'git update-server-info': %w", err)
}
// Now if we're an internal don't do anything else
@@ -496,10 +491,7 @@ func pushOptions() map[string]string {
return opts
}
-func runHookProcReceive(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runHookProcReceive(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
diff --git a/cmd/keys.go b/cmd/keys.go
index 7fdbe16119..8710756a81 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"strings"
@@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdKeys represents the available keys sub-command
@@ -49,7 +50,7 @@ var CmdKeys = &cli.Command{
},
}
-func runKeys(c *cli.Context) error {
+func runKeys(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") {
return errors.New("No username provided")
}
@@ -68,9 +69,6 @@ func runKeys(c *cli.Context) error {
return errors.New("No key type and content provided")
}
- ctx, cancel := installSignals()
- defer cancel()
-
setup(ctx, c.Bool("debug"))
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
@@ -78,6 +76,6 @@ func runKeys(c *cli.Context) error {
if extra.Error != nil {
return extra.Error
}
- _, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text))
+ _, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text))
return nil
}
diff --git a/cmd/mailer.go b/cmd/mailer.go
index 0c5f2c8c8d..72bd8e5601 100644
--- a/cmd/mailer.go
+++ b/cmd/mailer.go
@@ -4,24 +4,18 @@
package cmd
import (
+ "context"
"fmt"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-func runSendMail(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runSendMail(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()
- if err := argsSet(c, "title"); err != nil {
- return err
- }
-
subject := c.String("title")
confirmSkiped := c.Bool("force")
body := c.String("content")
diff --git a/cmd/main.go b/cmd/main.go
index 7251bd09a3..128b8776b4 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"fmt"
"os"
"strings"
@@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// cmdHelp is our own help subcommand with more information
@@ -22,18 +23,18 @@ func cmdHelp() *cli.Command {
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
- Action: func(c *cli.Context) (err error) {
- lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil}
+ Action: func(ctx context.Context, c *cli.Command) (err error) {
+ lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea
targetCmdIdx := 0
- if c.Command.Name == "help" {
+ if c.Name == "help" {
targetCmdIdx = 1
}
- if lineage[targetCmdIdx+1].Command != nil {
- err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name)
+ if lineage[targetCmdIdx] != lineage[targetCmdIdx].Root() {
+ err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1] /* parent cmd */, lineage[targetCmdIdx].Name /* sub cmd */)
} else {
err = cli.ShowAppHelp(c)
}
- _, _ = fmt.Fprintf(c.App.Writer, `
+ _, _ = fmt.Fprintf(c.Root().Writer, `
DEFAULT CONFIGURATION:
AppPath: %s
WorkPath: %s
@@ -74,25 +75,25 @@ func appGlobalFlags() []cli.Flag {
}
}
-func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) {
- command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...)
+func prepareSubcommandWithGlobalFlags(command *cli.Command) {
+ command.Flags = append(append([]cli.Flag{}, appGlobalFlags()...), command.Flags...)
command.Action = prepareWorkPathAndCustomConf(command.Action)
command.HideHelp = true
if command.Name != "help" {
- command.Subcommands = append(command.Subcommands, cmdHelp())
+ command.Commands = append(command.Commands, cmdHelp())
}
- for i := range command.Subcommands {
- prepareSubcommandWithConfig(command.Subcommands[i], globalFlags)
+ for i := range command.Commands {
+ prepareSubcommandWithGlobalFlags(command.Commands[i])
}
}
// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
-func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error {
- return func(ctx *cli.Context) error {
+func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(context.Context, *cli.Command) error {
+ return func(ctx context.Context, cmd *cli.Command) error {
var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags
- for _, curCtx := range ctx.Lineage() {
+ for _, curCtx := range cmd.Lineage() {
if curCtx.IsSet("work-path") && args.WorkPath == "" {
args.WorkPath = curCtx.String("work-path")
}
@@ -104,11 +105,11 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
}
}
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
- if ctx.Bool("help") || action == nil {
+ if cmd.Bool("help") || action == nil {
// the default behavior of "urfave/cli": "nil action" means "show help"
- return cmdHelp().Action(ctx)
+ return cmdHelp().Action(ctx, cmd)
}
- return action(ctx)
+ return action(ctx, cmd)
}
}
@@ -117,14 +118,13 @@ type AppVersion struct {
Extra string
}
-func NewMainApp(appVer AppVersion) *cli.App {
- app := cli.NewApp()
- app.Name = "Gitea"
- app.HelpName = "gitea"
+func NewMainApp(appVer AppVersion) *cli.Command {
+ app := &cli.Command{}
+ app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
app.Usage = "A painless self-hosted Git service"
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = appVer.Version + appVer.Extra
- app.EnableBashCompletion = true
+ app.EnableShellCompletion = true
// these sub-commands need to use config file
subCmdWithConfig := []*cli.Command{
@@ -147,20 +147,19 @@ func NewMainApp(appVer AppVersion) *cli.App {
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
subCmdStandalone := []*cli.Command{
- CmdCert,
+ cmdCert(),
CmdGenerate,
CmdDocs,
}
app.DefaultCommand = CmdWeb.Name
- globalFlags := appGlobalFlags()
app.Flags = append(app.Flags, cli.VersionFlag)
- app.Flags = append(app.Flags, globalFlags...)
+ app.Flags = append(app.Flags, appGlobalFlags()...)
app.HideHelp = true // use our own help action to show helps (with more information like default config)
app.Before = PrepareConsoleLoggerLevel(log.INFO)
for i := range subCmdWithConfig {
- prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags)
+ prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
}
app.Commands = append(app.Commands, subCmdWithConfig...)
app.Commands = append(app.Commands, subCmdStandalone...)
@@ -169,8 +168,10 @@ func NewMainApp(appVer AppVersion) *cli.App {
return app
}
-func RunMainApp(app *cli.App, args ...string) error {
- err := app.Run(args)
+func RunMainApp(app *cli.Command, args ...string) error {
+ ctx, cancel := installSignals()
+ defer cancel()
+ err := app.Run(ctx, args)
if err == nil {
return nil
}
diff --git a/cmd/main_test.go b/cmd/main_test.go
index 9573cacbd4..7dfa87a0ef 100644
--- a/cmd/main_test.go
+++ b/cmd/main_test.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"io"
@@ -16,7 +17,7 @@ import (
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func TestMain(m *testing.M) {
@@ -27,10 +28,10 @@ func makePathOutput(workPath, customPath, customConf string) string {
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
}
-func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App {
+func newTestApp(testCmdAction cli.ActionFunc) *cli.Command {
app := NewMainApp(AppVersion{})
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
- prepareSubcommandWithConfig(testCmd, appGlobalFlags())
+ prepareSubcommandWithGlobalFlags(testCmd)
app.Commands = append(app.Commands, testCmd)
app.DefaultCommand = testCmd.Name
return app
@@ -42,7 +43,7 @@ type runResult struct {
ExitCode int
}
-func runTestApp(app *cli.App, args ...string) (runResult, error) {
+func runTestApp(app *cli.Command, args ...string) (runResult, error) {
outBuf := new(strings.Builder)
errBuf := new(strings.Builder)
app.Writer = outBuf
@@ -65,7 +66,7 @@ func TestCliCmd(t *testing.T) {
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
cli.CommandHelpTemplate = "(command help template)"
- cli.AppHelpTemplate = "(app help template)"
+ cli.RootCommandHelpTemplate = "(app help template)"
cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct {
@@ -109,12 +110,12 @@ func TestCliCmd(t *testing.T) {
},
}
- app := newTestApp(func(ctx *cli.Context) error {
- _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
- return nil
- })
for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) {
+ app := newTestApp(func(ctx context.Context, cmd *cli.Command) error {
+ _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
+ return nil
+ })
for k, v := range c.env {
t.Setenv(k, v)
}
@@ -128,28 +129,28 @@ func TestCliCmd(t *testing.T) {
}
func TestCliCmdError(t *testing.T) {
- app := newTestApp(func(ctx *cli.Context) error { return errors.New("normal error") })
+ app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") })
r, err := runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Command error: normal error\n", r.Stderr)
- app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) })
+ app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) })
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 2, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "exit error\n", r.Stderr)
- app = newTestApp(func(ctx *cli.Context) error { return nil })
+ app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
- assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout)
- assert.Empty(t, r.Stderr) // the cli package's strange behavior, the error message is not in stderr ....
+ assert.Empty(t, r.Stdout)
+ assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
- app = newTestApp(func(ctx *cli.Context) error { return nil })
+ app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.NoError(t, err)
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
diff --git a/cmd/manager.go b/cmd/manager.go
index bd2da8edc7..f0935ea065 100644
--- a/cmd/manager.go
+++ b/cmd/manager.go
@@ -4,12 +4,13 @@
package cmd
import (
+ "context"
"os"
"time"
"code.gitea.io/gitea/modules/private"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -18,7 +19,7 @@ var (
Name: "manager",
Usage: "Manage the running gitea process",
Description: "This is a command for managing the running gitea process",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdShutdown,
subcmdRestart,
subcmdReloadTemplates,
@@ -108,46 +109,31 @@ var (
}
)
-func runShutdown(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runShutdown(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.Shutdown(ctx)
return handleCliResponseExtra(extra)
}
-func runRestart(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRestart(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.Restart(ctx)
return handleCliResponseExtra(extra)
}
-func runReloadTemplates(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runReloadTemplates(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.ReloadTemplates(ctx)
return handleCliResponseExtra(extra)
}
-func runFlushQueues(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runFlushQueues(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking"))
return handleCliResponseExtra(extra)
}
-func runProcesses(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runProcesses(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel"))
return handleCliResponseExtra(extra)
diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go
index c2ae25ec57..c83073e9c6 100644
--- a/cmd/manager_logging.go
+++ b/cmd/manager_logging.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"os"
@@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -60,7 +61,7 @@ var (
subcmdLogging = &cli.Command{
Name: "logging",
Usage: "Adjust logging commands",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
{
Name: "pause",
Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)",
@@ -104,7 +105,7 @@ var (
}, {
Name: "add",
Usage: "Add a logger",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
{
Name: "file",
Usage: "Add a file logger",
@@ -195,10 +196,7 @@ var (
}
)
-func runRemoveLogger(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRemoveLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
logger := c.String("logger")
if len(logger) == 0 {
@@ -210,10 +208,7 @@ func runRemoveLogger(c *cli.Context) error {
return handleCliResponseExtra(extra)
}
-func runAddConnLogger(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runAddConnLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "conn"
@@ -237,13 +232,10 @@ func runAddConnLogger(c *cli.Context) error {
if c.IsSet("reconnect-on-message") {
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
}
- return commonAddLogger(c, mode, vals)
+ return commonAddLogger(ctx, c, mode, vals)
}
-func runAddFileLogger(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runAddFileLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "file"
@@ -270,10 +262,10 @@ func runAddFileLogger(c *cli.Context) error {
if c.IsSet("compression-level") {
vals["compressionLevel"] = c.Int("compression-level")
}
- return commonAddLogger(c, mode, vals)
+ return commonAddLogger(ctx, c, mode, vals)
}
-func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error {
+func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
if len(c.String("level")) > 0 {
vals["level"] = log.LevelFromString(c.String("level")).String()
}
@@ -300,46 +292,33 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error {
if c.IsSet("writer") {
writer = c.String("writer")
}
- ctx, cancel := installSignals()
- defer cancel()
extra := private.AddLogger(ctx, logger, writer, mode, vals)
return handleCliResponseExtra(extra)
}
-func runPauseLogging(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runPauseLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.PauseLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
-func runResumeLogging(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runResumeLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.ResumeLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
-func runReleaseReopenLogging(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.ReleaseReopenLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
-func runSetLogSQL(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
+func runSetLogSQL(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.SetLogSQL(ctx, !c.Bool("off"))
diff --git a/cmd/migrate.go b/cmd/migrate.go
index 25d8b50c45..e24dc9e572 100644
--- a/cmd/migrate.go
+++ b/cmd/migrate.go
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/versioned_migration"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdMigrate represents the available migrate sub-command.
@@ -22,11 +22,8 @@ var CmdMigrate = &cli.Command{
Action: runMigrate,
}
-func runMigrate(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+func runMigrate(ctx context.Context, c *cli.Command) error {
+ if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go
index f9ed140395..2c63e15f50 100644
--- a/cmd/migrate_storage.go
+++ b/cmd/migrate_storage.go
@@ -22,7 +22,7 @@ import (
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/services/versioned_migration"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdMigrateStorage represents the available migrate storage sub-command.
@@ -213,11 +213,8 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora
})
}
-func runMigrateStorage(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
+ if err := initDB(ctx); err != nil {
return err
}
@@ -238,51 +235,51 @@ func runMigrateStorage(ctx *cli.Context) error {
var dstStorage storage.ObjectStorage
var err error
- switch strings.ToLower(ctx.String("storage")) {
+ switch strings.ToLower(cmd.String("storage")) {
case "":
fallthrough
case string(setting.LocalStorageType):
- p := ctx.String("path")
+ p := cmd.String("path")
if p == "" {
log.Fatal("Path must be given when storage is local")
return nil
}
dstStorage, err = storage.NewLocalStorage(
- stdCtx,
+ ctx,
&setting.Storage{
Path: p,
})
case string(setting.MinioStorageType):
dstStorage, err = storage.NewMinioStorage(
- stdCtx,
+ ctx,
&setting.Storage{
MinioConfig: setting.MinioStorageConfig{
- Endpoint: ctx.String("minio-endpoint"),
- AccessKeyID: ctx.String("minio-access-key-id"),
- SecretAccessKey: ctx.String("minio-secret-access-key"),
- Bucket: ctx.String("minio-bucket"),
- Location: ctx.String("minio-location"),
- BasePath: ctx.String("minio-base-path"),
- UseSSL: ctx.Bool("minio-use-ssl"),
- InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"),
- ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"),
- BucketLookUpType: ctx.String("minio-bucket-lookup-type"),
+ Endpoint: cmd.String("minio-endpoint"),
+ AccessKeyID: cmd.String("minio-access-key-id"),
+ SecretAccessKey: cmd.String("minio-secret-access-key"),
+ Bucket: cmd.String("minio-bucket"),
+ Location: cmd.String("minio-location"),
+ BasePath: cmd.String("minio-base-path"),
+ UseSSL: cmd.Bool("minio-use-ssl"),
+ InsecureSkipVerify: cmd.Bool("minio-insecure-skip-verify"),
+ ChecksumAlgorithm: cmd.String("minio-checksum-algorithm"),
+ BucketLookUpType: cmd.String("minio-bucket-lookup-type"),
},
})
case string(setting.AzureBlobStorageType):
dstStorage, err = storage.NewAzureBlobStorage(
- stdCtx,
+ ctx,
&setting.Storage{
AzureBlobConfig: setting.AzureBlobStorageConfig{
- Endpoint: ctx.String("azureblob-endpoint"),
- AccountName: ctx.String("azureblob-account-name"),
- AccountKey: ctx.String("azureblob-account-key"),
- Container: ctx.String("azureblob-container"),
- BasePath: ctx.String("azureblob-base-path"),
+ Endpoint: cmd.String("azureblob-endpoint"),
+ AccountName: cmd.String("azureblob-account-name"),
+ AccountKey: cmd.String("azureblob-account-key"),
+ Container: cmd.String("azureblob-container"),
+ BasePath: cmd.String("azureblob-base-path"),
},
})
default:
- return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))
+ return fmt.Errorf("unsupported storage type: %s", cmd.String("storage"))
}
if err != nil {
return err
@@ -299,14 +296,14 @@ func runMigrateStorage(ctx *cli.Context) error {
"actions-artifacts": migrateActionsArtifacts,
}
- tp := strings.ToLower(ctx.String("type"))
+ tp := strings.ToLower(cmd.String("type"))
if m, ok := migratedMethods[tp]; ok {
- if err := m(stdCtx, dstStorage); err != nil {
+ if err := m(ctx, dstStorage); err != nil {
return err
}
log.Info("%s files have successfully been copied to the new storage.", tp)
return nil
}
- return fmt.Errorf("unsupported storage: %s", ctx.String("type"))
+ return fmt.Errorf("unsupported storage: %s", cmd.String("type"))
}
diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go
index 37b32aa304..c61f5a582e 100644
--- a/cmd/restore_repo.go
+++ b/cmd/restore_repo.go
@@ -4,12 +4,13 @@
package cmd
import (
+ "context"
"strings"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdRestoreRepository represents the available restore a repository sub-command.
@@ -48,10 +49,7 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
},
}
-func runRestoreRepository(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRestoreRepository(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()
var units []string
if s := c.String("units"); s != "" {
diff --git a/cmd/serv.go b/cmd/serv.go
index 26a3af50f3..e4434450d6 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -33,7 +33,7 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/kballard/go-shellquote"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdServ represents the available serv sub-command.
@@ -152,10 +152,7 @@ func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServC
return "Bearer " + tokenString, nil
}
-func runServ(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runServ(ctx context.Context, c *cli.Command) error {
// FIXME: This needs to internationalised
setup(ctx, c.Bool("debug"))
diff --git a/cmd/web.go b/cmd/web.go
index e47b171455..39e336fe54 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -28,7 +28,7 @@ import (
"code.gitea.io/gitea/routers/install"
"github.com/felixge/fgprof"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// PIDFile could be set from build tag
@@ -130,19 +130,19 @@ func showWebStartupMessage(msg string) {
}
}
-func serveInstall(ctx *cli.Context) error {
+func serveInstall(cmd *cli.Command) error {
showWebStartupMessage("Prepare to run install page")
routers.InitWebInstallPage(graceful.GetManager().HammerContext())
// Flag for port number in case first time run conflict
- if ctx.IsSet("port") {
- if err := setPort(ctx.String("port")); err != nil {
+ if cmd.IsSet("port") {
+ if err := setPort(cmd.String("port")); err != nil {
return err
}
}
- if ctx.IsSet("install-port") {
- if err := setPort(ctx.String("install-port")); err != nil {
+ if cmd.IsSet("install-port") {
+ if err := setPort(cmd.String("install-port")); err != nil {
return err
}
}
@@ -163,7 +163,7 @@ func serveInstall(ctx *cli.Context) error {
return nil
}
-func serveInstalled(ctx *cli.Context) error {
+func serveInstalled(c *cli.Command) error {
setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings()
setting.MustInstalled()
@@ -218,8 +218,8 @@ func serveInstalled(ctx *cli.Context) error {
setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour)
// Override the provided port number within the configuration
- if ctx.IsSet("port") {
- if err := setPort(ctx.String("port")); err != nil {
+ if c.IsSet("port") {
+ if err := setPort(c.String("port")); err != nil {
return err
}
}
@@ -244,7 +244,7 @@ func servePprof() {
finished()
}
-func runWeb(ctx *cli.Context) error {
+func runWeb(_ context.Context, cmd *cli.Command) error {
defer func() {
if panicked := recover(); panicked != nil {
log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
@@ -262,12 +262,12 @@ func runWeb(ctx *cli.Context) error {
}
// Set pid file setting
- if ctx.IsSet("pid") {
- createPIDFile(ctx.String("pid"))
+ if cmd.IsSet("pid") {
+ createPIDFile(cmd.String("pid"))
}
if !setting.InstallLock {
- if err := serveInstall(ctx); err != nil {
+ if err := serveInstall(cmd); err != nil {
return err
}
} else {
@@ -278,7 +278,7 @@ func runWeb(ctx *cli.Context) error {
go servePprof()
}
- return serveInstalled(ctx)
+ return serveInstalled(cmd)
}
func setPort(port string) error {
diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go
index 44e4eacf90..630bd77531 100644
--- a/contrib/backport/backport.go
+++ b/contrib/backport/backport.go
@@ -12,21 +12,19 @@ import (
"net/http"
"os"
"os/exec"
- "os/signal"
"path"
"strconv"
"strings"
- "syscall"
"github.com/google/go-github/v71/github"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
"gopkg.in/yaml.v3"
)
const defaultVersion = "v1.18" // to backport to
func main() {
- app := cli.NewApp()
+ app := &cli.Command{}
app.Name = "backport"
app.Usage = "Backport provided PR-number on to the current or previous released version"
app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version`
@@ -91,7 +89,7 @@ func main() {
Usage: "Set this flag to continue from a git cherry-pick that has broken",
},
}
- cli.AppHelpTemplate = `NAME:
+ cli.RootCommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
@@ -105,16 +103,12 @@ OPTIONS:
`
app.Action = runBackport
-
- if err := app.Run(os.Args); err != nil {
+ if err := app.Run(context.Background(), os.Args); err != nil {
fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err)
}
}
-func runBackport(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runBackport(ctx context.Context, c *cli.Command) error {
continuing := c.Bool("continue")
var pr string
@@ -460,25 +454,3 @@ func determineSHAforPR(ctx context.Context, prStr, accessToken string) (string,
return "", nil
}
-
-func installSignals() (context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- go func() {
- // install notify
- signalChannel := make(chan os.Signal, 1)
-
- signal.Notify(
- signalChannel,
- syscall.SIGINT,
- syscall.SIGTERM,
- )
- select {
- case <-signalChannel:
- case <-ctx.Done():
- }
- cancel()
- signal.Reset()
- }()
-
- return ctx, cancel
-}
diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go
index a7d7a6d293..5eb576c6fe 100644
--- a/contrib/environment-to-ini/environment-to-ini.go
+++ b/contrib/environment-to-ini/environment-to-ini.go
@@ -4,16 +4,17 @@
package main
import (
+ "context"
"os"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func main() {
- app := cli.NewApp()
+ app := cli.Command{}
app.Name = "environment-to-ini"
app.Usage = "Use provided environment to update configuration ini"
app.Description = `As a helper to allow docker users to update the gitea configuration
@@ -72,13 +73,13 @@ func main() {
},
}
app.Action = runEnvironmentToIni
- err := app.Run(os.Args)
+ err := app.Run(context.Background(), os.Args)
if err != nil {
log.Fatal("Failed to run app with %s: %v", os.Args, err)
}
}
-func runEnvironmentToIni(c *cli.Context) error {
+func runEnvironmentToIni(_ context.Context, c *cli.Command) error {
// the config system may change the environment variables, so get a copy first, to be used later
env := append([]string{}, os.Environ()...)
setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{
diff --git a/go.mod b/go.mod
index eca7d773d0..d2305b4f12 100644
--- a/go.mod
+++ b/go.mod
@@ -110,7 +110,8 @@ require (
github.com/syndtr/goleveldb v1.0.0
github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.12
- github.com/urfave/cli/v2 v2.27.6
+ github.com/urfave/cli-docs/v3 v3.0.0-alpha6
+ github.com/urfave/cli/v3 v3.3.3
github.com/wneessen/go-mail v0.6.2
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1
@@ -186,7 +187,7 @@ require (
github.com/couchbase/go-couchbase v0.1.1 // indirect
github.com/couchbase/gomemcached v0.3.3 // indirect
github.com/couchbase/goutils v0.1.2 // indirect
- github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
@@ -296,7 +297,6 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
- github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
diff --git a/go.sum b/go.sum
index 6d76e0082b..090a04346d 100644
--- a/go.sum
+++ b/go.sum
@@ -223,8 +223,8 @@ github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9B
github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
-github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
-github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
+github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
@@ -761,8 +761,10 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
-github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
-github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
+github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI=
+github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU=
+github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
+github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
@@ -782,8 +784,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
-github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
diff --git a/main.go b/main.go
index 756c3e0f9b..2c25bac4e3 100644
--- a/main.go
+++ b/main.go
@@ -21,7 +21,7 @@ import (
_ "code.gitea.io/gitea/modules/markup/markdown"
_ "code.gitea.io/gitea/modules/markup/orgmode"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// these flags will be set by the build flags
diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go
index 61f11c58b0..3878302ef0 100644
--- a/tests/integration/cmd_keys_test.go
+++ b/tests/integration/cmd_keys_test.go
@@ -13,7 +13,7 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func Test_CmdKeys(t *testing.T) {
@@ -36,18 +36,21 @@ func Test_CmdKeys(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- out := new(bytes.Buffer)
- app := cli.NewApp()
- app.Writer = out
- app.Commands = []*cli.Command{cmd.CmdKeys}
+ var stdout, stderr bytes.Buffer
+ app := &cli.Command{
+ Writer: &stdout,
+ ErrWriter: &stderr,
+ Commands: []*cli.Command{cmd.CmdKeys},
+ }
cmd.CmdKeys.HideHelp = true
- err := app.Run(append([]string{"prog"}, tt.args...))
+ err := app.Run(t.Context(), append([]string{"prog"}, tt.args...))
if tt.wantErr {
assert.Error(t, err)
+ assert.Equal(t, tt.expectedOutput, stderr.String())
} else {
assert.NoError(t, err)
+ assert.Equal(t, tt.expectedOutput, stdout.String())
}
- assert.Equal(t, tt.expectedOutput, out.String())
})
}
})
From 1610a63bfd9e243a0d1ad8a5d05a5ae011a957a9 Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Tue, 10 Jun 2025 23:20:32 +0800
Subject: [PATCH 003/131] Fix commit message rendering and some UI problems
(#34680)
* Fix #34679
* Fix #34676
* Fix #34674
* Fix #34526
---
models/renderhelper/repo_comment.go | 24 +++++++++----------
models/renderhelper/repo_comment_test.go | 7 ++++++
modules/markup/html.go | 6 ++---
modules/markup/html_test.go | 4 ++++
modules/templates/util_render.go | 6 ++---
.../actions/workflow_dispatch_inputs.tmpl | 3 ++-
web_src/css/repo/issue-card.css | 1 +
7 files changed, 31 insertions(+), 20 deletions(-)
diff --git a/models/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go
index a400f7b908..ae0fbf0abd 100644
--- a/models/renderhelper/repo_comment.go
+++ b/models/renderhelper/repo_comment.go
@@ -48,10 +48,7 @@ type RepoCommentOptions struct {
}
func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext {
- helper := &RepoComment{
- repoLink: repo.Link(),
- opts: util.OptionalArg(opts),
- }
+ helper := &RepoComment{opts: util.OptionalArg(opts)}
rctx := markup.NewRenderContext(ctx)
helper.ctx = rctx
var metas map[string]string
@@ -60,15 +57,16 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor
helper.commitChecker = newCommitChecker(ctx, repo)
metas = repo.ComposeCommentMetas(ctx)
} else {
- // this is almost dead code, only to pass the incorrect tests
- helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
- rctx = rctx.WithMetas(map[string]string{
- "user": helper.opts.DeprecatedOwnerName,
- "repo": helper.opts.DeprecatedRepoName,
-
- "markdownNewLineHardBreak": "true",
- "markupAllowShortIssuePattern": "true",
- })
+ // repo can be nil when rendering a commit message in user's dashboard feedback whose repository has been deleted
+ metas = map[string]string{}
+ if helper.opts.DeprecatedOwnerName != "" {
+ // this is almost dead code, only to pass the incorrect tests
+ helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
+ metas["user"] = helper.opts.DeprecatedOwnerName
+ metas["repo"] = helper.opts.DeprecatedRepoName
+ }
+ metas["markdownNewLineHardBreak"] = "true"
+ metas["markupAllowShortIssuePattern"] = "true"
}
metas["footnoteContextId"] = helper.opts.FootnoteContextID
rctx = rctx.WithMetas(metas).WithHelper(helper)
diff --git a/models/renderhelper/repo_comment_test.go b/models/renderhelper/repo_comment_test.go
index 776152db96..3b13bff73c 100644
--- a/models/renderhelper/repo_comment_test.go
+++ b/models/renderhelper/repo_comment_test.go
@@ -72,4 +72,11 @@ func TestRepoComment(t *testing.T) {

`, rendered)
})
+
+ t.Run("NoRepo", func(t *testing.T) {
+ rctx := NewRenderContextRepoComment(t.Context(), nil).WithMarkupType(markdown.MarkupName)
+ rendered, err := markup.RenderString(rctx, "any")
+ assert.NoError(t, err)
+ assert.Equal(t, "any
\n", rendered)
+ })
}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index d45153d95b..e8391341d9 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -86,8 +86,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
- // cleans: ""
strings.NewReader(""),
- // Strip out nuls - they're always invalid
+ // strip out NULLs (they're always invalid), and escape known tags
bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))),
// close the tags
strings.NewReader(""),
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 58f71bdd7b..5fdbf43f7c 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -525,6 +525,10 @@ func TestPostProcess(t *testing.T) {
test("", `<script>a</script>`)
test("", `<style>a</STYLE>`)
+
+ // other special tags, our special behavior
+ test("
{{end}}
-
+ {{/* use autofocus here to prevent the "branch selection" dropdown from getting focus, otherwise it will auto popup */}}
+
{{end}}
{{range .workflows}}
diff --git a/web_src/css/repo/issue-card.css b/web_src/css/repo/issue-card.css
index fb832bd05a..27f3c2d554 100644
--- a/web_src/css/repo/issue-card.css
+++ b/web_src/css/repo/issue-card.css
@@ -7,6 +7,7 @@
padding: 8px 10px;
border: 1px solid var(--color-secondary);
background: var(--color-card);
+ color: var(--color-text); /* it can't inherit from parent because the card already has its own background */
}
.issue-card-icon,
From 17cfae82a5e8357f90701815b11c9bc615d0f7e8 Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Wed, 11 Jun 2025 03:32:20 +0800
Subject: [PATCH 004/131] Hide href attribute of a tag if there is no
target_url (#34556)
Relate #34450
---
web_src/js/components/DashboardRepoList.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 6b16ff9efb..6573633227 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -429,7 +429,7 @@ export default defineComponent({
-
+
From d5afdccde82e7f00e54d140533308fe76bf41783 Mon Sep 17 00:00:00 2001
From: GiteaBot
Date: Wed, 11 Jun 2025 00:37:02 +0000
Subject: [PATCH 005/131] [skip ci] Updated translations via Crowdin
---
options/locale/locale_uk-UA.ini | 76 ++-
options/locale/locale_zh-CN.ini | 918 ++++++++++++++++----------------
2 files changed, 496 insertions(+), 498 deletions(-)
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 42df483d8d..da38ca30b0 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -40,7 +40,7 @@ passcode=Код доступу
webauthn_insert_key=Вставте ключ безпеки
webauthn_sign_in=Натисніть кнопку на вашому ключі безпеки. Якщо ваш ключ без фізичної кнопки, поновно вставте ключ.
webauthn_press_button=Будь ласка, натисніть кнопку на вашому ключі безпеки…
-webauthn_use_twofa=Використовуйте дво-факторний код із Вашого телефона
+webauthn_use_twofa=Використовуйте двофакторний код із Вашого телефона
webauthn_error=Не вдалося прочитати ваш ключ безпеки.
webauthn_unsupported_browser=Ваш браузер наразі не підтримує WebAuthn.
webauthn_error_unknown=Сталася невідома помилка. Будь ласка, спробуйте ще раз.
@@ -51,11 +51,11 @@ webauthn_error_empty=Ви повинні встановити назву для
webauthn_error_timeout=Час очікування вичерпано, перш ніж ваш ключ було прочитано. Перезавантажте сторінку та спробуйте ще раз.
webauthn_reload=Оновити
-repository=Репозиторій
+repository=Сховище
organization=Організація
mirror=Дзеркало
issue_milestone=Етап
-new_repo=Новий репозиторій
+new_repo=Нове сховище
new_migrate=Нова міграція
new_mirror=Нове дзеркало
new_org=Нова організація
@@ -131,9 +131,9 @@ artifacts=Артефакти
expired=Прострочено
confirm_delete_artifact=Справді видалити артефакт '%s' ?
-archived=Архівовані
+archived=Архівовано
-concept_code_repository=Репозиторій
+concept_code_repository=Сховище
concept_user_organization=Організація
show_timestamps=Показувати часові мітки
@@ -148,7 +148,7 @@ value=Значення
filter=Фільтр
filter.clear=Очистити фільтр
-filter.is_archived=Архівовані
+filter.is_archived=Архівовано
filter.not_archived=Не архівовано
filter.is_fork=Відгалужено
filter.not_fork=Не відгалужено
@@ -224,70 +224,68 @@ app_desc=Зручний власний сервіс хостингу репоз
install=Легко встановити
platform=Платформонезалежність
lightweight=Невибагливість
-lightweight_desc=Gitea має низькі вимоги до ресурсів та може працювати на недорогому Raspberry Pi. Збережіть свою машину енергію!
license=Відкритий вихідний код
[install]
install=Встановлення
installing_desc=Встановлення, будь ласка, зачекайте...
title=Початкова конфігурація
-docker_helper=Якщо ви запускаєте Gitea всередині Docker, будь ласка уважно прочитайте документацію перед тим, як щось змінити на цій сторінці.
-require_db_desc=Gitea вимагає MySQL, PostgreSQL, MSSQL, SQLite3 або TiDB (протокол MySQL).
+docker_helper=Якщо ви запускаєте Gitea у Docker, будь ласка, прочитайте документацію перед тим, як змінювати будь-які налаштування.
+require_db_desc=Gitea потребує MySQL, PostgreSQL, MSSQL, SQLite3 або TiDB (протокол MySQL).
db_title=Налаштування бази даних
db_type=Тип бази даних
host=Хост
user=Ім'я кристувача
password=Пароль
-db_name=Ім'я бази даних
+db_name=Назва бази даних
db_schema=Схема
-db_schema_helper=Залиште пустим для бази даних за замовчуванням ("публічна").
+db_schema_helper=Залиште пустим для типової схеми бази даних ("публічна").
ssl_mode=SSL
path=Шлях
-sqlite_helper=Шлях до файлу для бази даних SQLite3.
Введіть абсолютний шлях, якщо ви запускаєте Gіtea як сервіс.
+sqlite_helper=Шлях до файлу бази даних SQLite3.
Введіть абсолютний шлях, якщо ви запускаєте Gіtea як сервіс.
reinstall_error=Ви намагаєтеся встановити в наявну базу даних Gitea
reinstall_confirm_message=Повторне встановлення в наявну базу даних Gitea може спричинити багато проблем. В більшості випадків, ви повинні використовувати свій наявний "app.ini" для запуску Gitea. Якщо ви знаєте, що робите, спробуйте наступне:
-reinstall_confirm_check_1=Дані зашифровані з використанням SECRET_KEY з app.ini можуть бути втрачені: користувачі не зможуть увійти з 2FA/OTP і дзеркала можуть працювати некоректно. Встановлюючи цей прапорець, ви підтверджуєте, що в поточному файлі app.ini вказано правильне значення SECRET_KEY.
-reinstall_confirm_check_2=Репозиторії та налаштування необхідно повторно синхронізувати. Встановлюючи цей прапорець, ви підтверджуєте, що ви синхронізуватимете хуки репозиторіїв та authorized_keys вручну. Ви підтверджуєте, що налаштування репозиторію і дзеркала є правильними.
-reinstall_confirm_check_3=Ви підтверджуєте, що повністю впевнені в тому, що для цього екземпляра Gitea вказано правильне розташування app.ini та екземпляр слід встановити повторно. Ви підтверджуєте, що усвідомлюєте вищенаведені ризики.
+reinstall_confirm_check_1=Дані, зашифровані за допомогою SECRET_KEY в app.ini, можуть бути втрачені: користувачі не зможуть увійти за допомогою 2FA/OTP, а дзеркала можуть працювати некоректно. Встановивши цей прапорець, ви підтверджуєте, що поточний файл app.ini містить правильний SECRET_KEY.
+reinstall_confirm_check_2=Можливо, потрібно буде повторно синхронізувати сховища та налаштування. Встановивши цей прапорець, ви підтверджуєте, що будете ресинхронізувати хуки для сховищ і файл authorized_keys вручну. Ви підтверджуєте, що забезпечите правильність налаштувань сховища і дзеркала.
+reinstall_confirm_check_3=Ви підтверджуєте, що абсолютно впевнені, що Gitea працює з правильним розташуванням файлу app.ini, і що вам потрібно перевстановити програму. Ви підтверджуєте, що усвідомлюєте вищевказані ризики.
err_empty_db_path=Шлях до файлу бази даних SQLite3 не може бути порожнім.
-no_admin_and_disable_registration=Ви не можете вимкнути реєстрацію до створення облікового запису адміністратора.
+no_admin_and_disable_registration=Ви не можете вимкнути реєстрацію без створення облікового запису адміністратора.
err_empty_admin_password=Пароль адміністратора не може бути порожнім.
err_empty_admin_email=Електронна адреса адміністратора не може бути порожньою.
-err_admin_name_is_reserved=Неправильне ім'я користувача-адміністратора - ім'я зарезервоване
-err_admin_name_pattern_not_allowed=Ім'я адміністратора недійсне, це ім'я підпадає під зарезервований шаблон
-err_admin_name_is_invalid=Неправильне ім'я користувача-адміністратора
+err_admin_name_is_reserved=Неправильне ім'я користувача адміністратора - ім'я зарезервовано
+err_admin_name_pattern_not_allowed=Ім'я користувача адміністратора недійсне, воно відповідає зарезервованому шаблону
+err_admin_name_is_invalid=Недійсне ім'я користувача адміністратора
general_title=Загальні налаштування
app_name=Назва сайту
app_name_helper=Тут ви можете ввести назву своєї компанії.
-repo_path=Кореневий шлях репозиторія
-repo_path_helper=Всі вилучені Git репозиторії будуть збережені в цей каталог.
-lfs_path=Кореневої шлях Git LFS
-lfs_path_helper=У цій папці будуть зберігатися файли Git LFS. Залиште порожнім, щоб вимкнути LFS.
-run_user=Запуск від імені Користувача
+repo_path=Кореневий шлях сховища
+repo_path_helper=До цього каталогу буде збережено віддалені сховища Git.
+lfs_path=Кореневий шлях Git LFS
+lfs_path_helper=У цій теці будуть зберігатися файли Git LFS. Залиште порожнім, щоб вимкнути.
+run_user=Виконати як
domain=Домен сервера
-domain_helper=Домен або адреса хоста сервера.
+domain_helper=Домен або хост-адреса сервера.
ssh_port=Порт SSH сервера
ssh_port_helper=Номер порту, який використовує SSH сервер. Залиште порожнім, щоб вимкнути SSH.
http_port=Gitea HTTP порт
-http_port_helper=Номер порту, який буде прослуховуватися Giteas веб-сервером.
+http_port_helper=Номер порту, який буде прослуховуватися сервером Giteas.
app_url=Базова URL-адреса Gitea
-app_url_helper=Базова адреса для HTTP(S) клонування через URL та повідомлень електронної пошти.
-log_root_path=Шлях до лог файлу
-log_root_path_helper=Файли журналу будуть записані в цей каталог.
+app_url_helper=Базова адреса для URL-адрес клонів HTTP(S) та сповіщень електронною поштою.
+log_root_path=Шлях до журналу
+log_root_path_helper=Файли журналу будуть записані в цю теку.
-optional_title=Додаткові налаштування
-email_title=Налаштування Email
-smtp_addr=SMTP хост
-smtp_port=SMTP порт
-smtp_from=Відправляти Email від імені
+optional_title=Необов'язкові налаштування
+email_title=Налаштування електронної пошти
+smtp_addr=Сервер SMTP
+smtp_port=Порт SMTP
smtp_from_invalid=Адреса "Надіслати листа як" недійсна
-smtp_from_helper=Електронна пошта для використання в Gіtea. Введіть звичайну електронну адресу або використовуйте формат: "Ім'я" .
-mailer_user=SMTP Ім'я кристувача
-mailer_password=SMTP Пароль
-register_confirm=Потрібно підтвердити електронну пошту для реєстрації
+smtp_from_helper=Адреса електронної пошти, яку буде використовувати Gitea. Введіть звичайну адресу електронної пошти або використовуйте формат «Ім'я» .
+mailer_user=Ім'я користувача SMTP
+mailer_password=Пароль SMTP
+register_confirm=Вимагати підтвердження електронною поштою для реєстрації
mail_notify=Увімкнути сповіщення електронною поштою
-server_service_title=Сервер і налаштування зовнішніх служб
+server_service_title=Налаштування сервера і сторонніх сервісів
offline_mode=Увімкнути локальний режим
offline_mode_popup=Відключити сторонні мережі доставки контенту і обслуговувати всі ресурси локально.
disable_gravatar=Вимкнути Gravatar
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 285ce7666f..9593e9d442 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -4,14 +4,14 @@ explore=探索
help=帮助
logo=徽标
sign_in=登录
-sign_in_with_provider=使用 %s 登录
+sign_in_with_provider=使用「%s」登录
sign_in_or=或
sign_out=退出
sign_up=注册
link_account=链接账户
register=注册
-version=当前版本
-powered_by=Powered by %s
+version=版本
+powered_by=由 %s 强力驱动
page=页面
template=模板
language=语言选项
@@ -28,7 +28,7 @@ return_to_gitea=返回 Gitea
more_items=更多选项
username=用户名
-email=电子邮件地址
+email=邮箱地址
password=密码
access_token=访问令牌(Access Token)
re_type=确认密码
@@ -42,7 +42,7 @@ webauthn_sign_in=按下安全密钥上的按钮。如果安全密钥没有按钮
webauthn_press_button=请按下安全密钥上的按钮…
webauthn_use_twofa=使用来自手机中的两步验证码
webauthn_error=无法读取安全密钥。
-webauthn_unsupported_browser=你的浏览器目前不支持 WebAuthn。
+webauthn_unsupported_browser=您的浏览器目前不支持 WebAuthn。
webauthn_error_unknown=发生未知错误。请重试。
webauthn_error_insecure=WebAuthn 仅支持安全连接。如果要在 HTTP 协议上进行测试,请使用 "localhost" 或 "127.0.0.1" 作为访问来源
webauthn_error_unable_to_process=服务器无法处理您的请求。
@@ -58,11 +58,11 @@ issue_milestone=里程碑
new_repo=创建仓库
new_migrate=迁移外部仓库
new_mirror=创建新的镜像
-new_fork=新的仓库Fork
+new_fork=派生新仓库
new_org=创建组织
new_project=创建项目
new_project_column=创建列
-manage_org=管理我的组织
+manage_org=管理组织
admin_panel=管理后台
account_settings=帐户设置
settings=设置
@@ -78,7 +78,7 @@ forks=派生
activities=最近活动
pull_requests=合并请求
-issues=工单管理
+issues=工单
milestones=里程碑
ok=确定
@@ -91,7 +91,7 @@ add=添加
add_all=添加所有
remove=移除
remove_all=移除所有
-remove_label_str=`删除标签 "%s"`
+remove_label_str=删除标签「%s」
edit=编辑
view=查看
test=测试
@@ -129,9 +129,9 @@ rss_feed=RSS 订阅源
pin=固定
unpin=取消置顶
-artifacts=制品
+artifacts=产物
expired=已过期
-confirm_delete_artifact=您确定要删除制品'%s'吗?
+confirm_delete_artifact=您确定要删除产物「%s」吗?
archived=已归档
@@ -165,7 +165,7 @@ filter.public=公开
filter.private=私有
no_results_found=未找到结果
-internal_error_skipped=发生内部错误,但已被跳过: %s
+internal_error_skipped=发生内部错误,但已跳过: %s
[search]
search=搜索...
@@ -184,14 +184,14 @@ org_kind=搜索组织...
team_kind=搜索团队...
code_kind=搜索代码...
code_search_unavailable=代码搜索当前不可用。请与网站管理员联系。
-code_search_by_git_grep=当前代码搜索结果由“git grep”提供。如果站点管理员启用仓库索引器,可能会有更好的结果。
+code_search_by_git_grep=当前代码搜索结果由「git grep」提供。如果站点管理员启用仓库索引器,可能会有更好的结果。
package_kind=搜索软件包...
project_kind=搜索项目...
branch_kind=搜索分支...
tag_kind=搜索标签...
-tag_tooltip=搜索匹配的标签。使用“%”来匹配任何序列的数字
+tag_tooltip=搜索匹配的标签。使用「%」来匹配任何序列的数字。
commit_kind=搜索提交记录...
-runner_kind=搜索runners...
+runner_kind=搜索运行器...
no_results=未找到匹配结果
issue_kind=搜索工单...
pull_kind=搜索合并请求...
@@ -268,14 +268,14 @@ ssl_mode=SSL
path=数据库文件路径
sqlite_helper=SQLite3 数据库的文件路径。
如果以服务的方式运行 Gitea,请输入绝对路径。
reinstall_error=您正在尝试安装到一个已经有 Gitea 数据的数据库中
-reinstall_confirm_message=使用现有的 Gitea 数据库重新安装可能会导致多个问题。在大多数情况下,你应该使用你现有的 “app.ini” 来运行 Gitea。如果你知道自己在做什么,请确认以下内容:
+reinstall_confirm_message=使用现有的 Gitea 数据库重新安装可能会导致多个问题。在大多数情况下,您应该使用您现有的「app.ini」来运行 Gitea。如果您知道自己在做什么,请确认以下内容:
reinstall_confirm_check_1=使用 app.ini 中 SECRET KEY 加密的数据可能会丢失:用户可能无法使用 2FA/OTP 登录,仓库镜像可能无法正常工作。勾选此框,表示您确认当前 app.ini 文件包含正确的 SECRET KEY。
reinstall_confirm_check_2=代码仓库和设置可能需要重新同步。勾选此框,表示您确认将手动重新同步仓库和 SSH authorized_keys 的钩子。您确认您将确保代码仓库和镜像设置是正确的。
-reinstall_confirm_check_3=你确认你绝对肯定这个 Gitea 在正确的 app.ini 位置上运行,而且你确定你必须重新安装。你确认你知晓上述风险。
+reinstall_confirm_check_3=您确认您绝对肯定这个 Gitea 在正确的 app.ini 位置上运行,而且您确定您必须重新安装。您确认您知晓上述风险。
err_empty_db_path=SQLite 数据库文件路径不能为空。
no_admin_and_disable_registration=您不能够在未创建管理员用户的情况下禁止注册。
err_empty_admin_password=管理员密码不能为空。
-err_empty_admin_email=管理员电子邮件不能为空。
+err_empty_admin_email=管理员邮箱不能为空。
err_admin_name_is_reserved=管理员用户名无效,用户名是保留的
err_admin_name_pattern_not_allowed=管理员用户名无效,用户名是保留字
err_admin_name_is_invalid=管理员用户名无效
@@ -296,7 +296,7 @@ ssh_port_helper=SSH 服务器的端口号,为空则禁用它。
http_port=HTTP 服务端口
http_port_helper=Giteas web 服务器将侦听的端口号。
app_url=基础URL
-app_url_helper=用于 HTTP (S) 克隆和电子邮件通知的基本地址。
+app_url_helper=用于 HTTP (S) 克隆和邮件通知的基本地址。
log_root_path=日志路径
log_root_path_helper=日志文件将写入此目录。
@@ -304,12 +304,12 @@ optional_title=可选设置
email_title=电子邮箱设置
smtp_addr=SMTP 主机地址
smtp_port=SMTP 端口
-smtp_from=电子邮件发件人
-smtp_from_invalid=`"发送电子邮件为"地址无效`
-smtp_from_helper=请输入一个用于 Gitea 的电子邮件地址,或者使用完整格式:"名称"
+smtp_from=邮件发件人
+smtp_from_invalid=「邮件发件人」地址无效
+smtp_from_helper=请输入一个用于 Gitea 的邮箱地址,或者使用完整格式:「名称」。
mailer_user=SMTP 用户名
mailer_password=SMTP 密码
-register_confirm=需要发电子邮件确认注册
+register_confirm=需要邮件确认注册
mail_notify=启用邮件通知提醒
server_service_title=服务器和第三方服务设置
offline_mode=启用本地模式
@@ -334,12 +334,12 @@ admin_title=管理员帐号设置
admin_name=管理员用户名
admin_password=管理员密码
confirm_password=确认密码
-admin_email=电子邮件地址
+admin_email=邮箱地址
install_btn_confirm=立即安装
test_git_failed=无法识别 'git' 命令:%v
sqlite3_not_available=您所使用的发行版不支持 SQLite3,请从 %s 下载官方构建版,而不是 gobuild 版本。
invalid_db_setting=数据库设置无效: %v
-invalid_db_table=数据库表 '%s' 无效: %v
+invalid_db_table=数据库表「%s」无效:%v
invalid_repo_path=仓库根目录设置无效:%v
invalid_app_data_path=应用数据路径无效: %v
run_user_not_match=运行用户名不是当前的用户名:%s -> %s
@@ -348,14 +348,14 @@ secret_key_failed=生成密钥失败: %v
save_config_failed=应用配置保存失败:%v
invalid_admin_setting=管理员帐户设置无效: %v
invalid_log_root_path=日志路径无效: %v
-default_keep_email_private=默认情况下隐藏电子邮件地址
-default_keep_email_private_popup=默认情况下, 隐藏新用户帐户的电子邮件地址。
+default_keep_email_private=默认情况下隐藏邮箱地址
+default_keep_email_private_popup=默认情况下,隐藏新用户帐户的邮箱地址。
default_allow_create_organization=默认情况下允许创建组织
default_allow_create_organization_popup=默认情况下, 允许新用户帐户创建组织。
default_enable_timetracking=默认情况下启用时间跟踪
default_enable_timetracking_popup=默认情况下启用新仓库的时间跟踪。
-no_reply_address=隐藏电子邮件
-no_reply_address_helper=具有隐藏电子邮件地址的用户的域名。例如, 用户名 "joe" 将以 "joe@noreply.example.org" 的身份登录到 Git 中. 如果隐藏的电子邮件域设置为 "noreply.example.org"。
+no_reply_address=隐藏邮件域
+no_reply_address_helper=具有隐藏邮箱地址的用户的域名。例如,如果隐藏邮箱域名设置为「noreply.example.org」,那么用户名「joe」在 Git 中将显示为「joe@noreply.example.org」。
password_algorithm=密码哈希算法
invalid_password_algorithm=无效的密码哈希算法
password_algorithm_helper=设置密码散列算法。算法有不同的要求和强度。 argon2 算法相当安全,但使用大量内存,因此可能不适合小型系统。
@@ -378,7 +378,7 @@ my_mirrors=我的镜像
view_home=访问 %s
filter=其他过滤器
filter_by_team_repositories=按团队仓库筛选
-feed_of=`"%s"的源`
+feed_of=「%s」的源
show_archived=已归档
show_both_archived_unarchived=显示已归档和未归档的
@@ -414,7 +414,7 @@ create_new_account=注册帐号
already_have_account=已有账号?
sign_in_now=立即登录
disable_register_prompt=对不起,注册功能已被关闭。请联系网站管理员。
-disable_register_mail=已禁用注册的电子邮件确认。
+disable_register_mail=已禁用注册邮件确认。
manual_activation_only=请联系您的站点管理员来完成激活。
remember_me=记住此设备
remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。
@@ -423,34 +423,34 @@ forgot_password=忘记密码?
need_account=需要一个帐户?
sign_up_now=还没账号?马上注册。
sign_up_successful=帐户创建成功。欢迎!
-confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 %s请在下一个 %s 中检查您的收件箱以完成注册过程。 如果您的注册电子邮件地址不正确,您可以重新登录并更改它。
+confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 %s。请在下一个 %s 中检查您的收件箱以完成注册流程。 如果您的注册邮箱地址不正确,您可以重新登录并更改它。
must_change_password=更新您的密码
allow_password_change=要求用户更改密码(推荐)
-reset_password_mail_sent_prompt=确认电子邮件已被发送到 %s。请您在 %s 内检查您的收件箱 ,完成密码重置过程。
+reset_password_mail_sent_prompt=确认邮件已被发送到 %s。请您在 %s 内检查您的收件箱 ,完成密码重置流程。
active_your_account=激活您的帐户
account_activated=帐户已激活
prohibit_login=禁止登录
prohibit_login_desc=您的帐户被禁止登录,请与网站管理员联系。
resent_limit_prompt=您请求发送激活邮件过于频繁,请等待 3 分钟后再试!
has_unconfirmed_mail=%s 您好,系统检测到您有一封发送至 %s 但未被确认的邮件。如果您未收到激活邮件,或需要重新发送,请单击下方的按钮。
-change_unconfirmed_mail_address=如果您的注册电子邮件地址不正确,您可以在此更改并重新发送新的确认电子邮件。
+change_unconfirmed_mail_address=如果您的注册邮箱地址不正确,您可以在此更改并重新发送新的确认邮件。
resend_mail=单击此处重新发送确认邮件
email_not_associate=您输入的邮箱地址未被关联到任何帐号!
send_reset_mail=发送账户恢复邮件
reset_password=账户恢复
invalid_code=此确认密钥无效或已过期。
-invalid_code_forgot_password=你的确认码无效或者已过期,点击 这里 开始新的会话。
+invalid_code_forgot_password=您的确认码无效或已过期,点击 这里 开始新的会话。
invalid_password=您的密码与用于创建账户的密码不匹配。
reset_password_helper=恢复账户
reset_password_wrong_user=您以 %s 登录,但恢复账号链接是用于 %s。
password_too_short=密码长度不能少于 %d 位。
-non_local_account=非本地帐户不能通过 Gitea 的 web 界面更改密码。
+non_local_account=非本地帐户不能通过 Gitea 的 Web 界面更改密码。
verify=验证
scratch_code=验证口令
use_scratch_code=使用验证口令
-twofa_scratch_used=你已经使用了你的验证口令。你将会转到两步验证设置页面以便移除你的注册设备或者重新生成新的验证口令。
-twofa_passcode_incorrect=你的验证码不正确。如果你丢失了你的设备,请使用你的验证口令。
-twofa_scratch_token_incorrect=你的验证口令不正确。
+twofa_scratch_used=您已经使用了您的验证口令。您将会转到两步验证设置页面以便移除您的注册设备或者重新生成新的验证口令。
+twofa_passcode_incorrect=您的验证码不正确。如果您丢失了您的设备,请使用您的验证口令。
+twofa_scratch_token_incorrect=您的验证口令不正确。
twofa_required=您必须设置两步验证来访问仓库,或者尝试重新登录。
login_userpass=登录
login_openid=OpenID
@@ -470,14 +470,14 @@ openid_connect_desc=所选的 OpenID URI 未知。在这里关联一个新帐户
openid_register_title=创建新帐户
openid_register_desc=所选的 OpenID URI 未知。在这里关联一个新帐户。
openid_signin_desc=输入您的OpenID地址。例如:alice.openid.example.org 或 https://openid.example.org/alice.
-disable_forgot_password_mail=由于未设置电子邮件,帐户恢复被禁用。 请联系您的站点管理员。
-disable_forgot_password_mail_admin=帐户恢复仅在设置电子邮件后可用。 请设置电子邮件以启用帐户恢复。
-email_domain_blacklisted=您不能使用您的电子邮件地址注册。
+disable_forgot_password_mail=由于未设置邮箱,帐户恢复被禁用。 请联系您的站点管理员。
+disable_forgot_password_mail_admin=帐户恢复仅在设置邮箱后可用。 请设置邮箱以启用帐户恢复。
+email_domain_blacklisted=您不能使用您的邮箱地址注册。
authorize_application=应用授权
authorize_redirect_notice=如果您授权此应用,您将会被重定向到 %s。
-authorize_application_created_by=此应用由%s创建。
+authorize_application_created_by=此应用由 %s 创建。
authorize_application_description=如果您允许,它将能够读取和修改您的所有帐户信息,包括私人仓库和组织。
-authorize_application_with_scopes=范围: %s
+authorize_application_with_scopes=范围:%s
authorize_title=授权 %s 访问您的帐户?
authorization_failed=授权失败
authorization_failed_desc=因为检测到无效请求,授权失败。请尝试联系您授权应用的管理员。
@@ -501,17 +501,17 @@ activate_account.text_2=请在 %s 时间内,点击以下链接激活您
activate_email=请验证您的邮箱地址
activate_email.title=%s,请验证您的邮箱
-activate_email.text=请在 %s 时间内,点击以下链接,以验证你的电子邮件地址:
+activate_email.text=请在 %s 时间内,点击以下链接,以验证您的邮箱地址:
register_notify=欢迎来到 %s
register_notify.title=%[1]s,欢迎来到 %[2]s
-register_notify.text_1=这是您的 %s 注册确认电子邮件 !
+register_notify.text_1=这是您的 %s 注册确认邮件 !
register_notify.text_2=您现在可以以用户名 %s 登录。
register_notify.text_3=如果此账户已为您创建,请先 设置您的密码。
reset_password=恢复您的账户
reset_password.title=%s,您已请求恢复您的帐户
-reset_password.text=请在 %s 时间内,点击以下链接,恢复你的账户:
+reset_password.text=请在 %s 时间内,点击以下链接,以恢复您的账户:
register_success=注册成功
@@ -533,26 +533,26 @@ issue.action.ready_for_review=@%[1]s 标记此合并请求已评审通过
issue.action.new=@%[1]s 创建了 #%[2]d.
issue.in_tree_path=在 %s 中:
-release.new.subject=%[2]s 中的 %[1]s 发布了
+release.new.subject=%[2]s 中的 %[1]s 已发布
release.new.text=@%[1]s 于 %[3]s 发布了 %[2]s
-release.title=标题: %s
+release.title=标题:%s
release.note=注释:
release.downloads=下载:
-release.download.zip=源代码 (ZIP)
-release.download.targz=源代码 (TAR.GZ)
+release.download.zip=源代码(ZIP)
+release.download.targz=源代码(TAR.GZ)
-repo.transfer.subject_to=%s 想要将 "%s" 转让给 %s
-repo.transfer.subject_to_you=%s 想要将 "%s" 转让给你
-repo.transfer.to_you=你
+repo.transfer.subject_to=%s 想要将「%s」转移给 %s
+repo.transfer.subject_to_you=%s 想要将「%s」转移给您
+repo.transfer.to_you=您
repo.transfer.body=访问 %s 以接受或拒绝转移,亦可忽略此邮件。
-repo.collaborator.added.subject=%s 把你添加到了 %s
-repo.collaborator.added.text=您已被添加为代码库的协作者:
+repo.collaborator.added.subject=%s 把您添加到了 %s
+repo.collaborator.added.text=您已被添加为仓库的协作者:
team_invite.subject=%[1]s 邀请您加入组织 %[2]s
team_invite.text_1=%[1]s 邀请您加入组织 %[3]s 中的团队 %[2]s。
team_invite.text_2=请点击下面的链接加入团队:
-team_invite.text_3=注意:这是发送给 %[1]s 的邀请。如果您未曾收到过此类邀请,请忽略这封电子邮件。
+team_invite.text_3=注意:此邀请是发送给 %[1]s 的。如果您未预期收到此邀请,请忽略这封邮件。
[modal]
yes=确认操作
@@ -592,9 +592,9 @@ size_error=长度必须为 %s。
min_size_error=长度最小为 %s 个字符。
max_size_error=长度最大为 %s 个字符。
email_error=不是一个有效的邮箱地址。
-url_error=`'%s' 不是一个有效的 URL。`
-include_error=`必须包含子字符串 "%s"。`
-glob_pattern_error=`匹配模式无效:%s.`
+url_error=`「%s」不是一个有效的 URL。`
+include_error=`必须包含子字符串「%s」。`
+glob_pattern_error=`匹配表达式无效:%s.`
regex_pattern_error=`正则表达式无效:%s.`
username_error=` 只能包含字母数字字符('0-9','a-z','A-Z'), 破折号 ('-'), 下划线 ('_') 和点 ('.'). 不能以非字母数字字符开头或结尾,并且不允许连续的非字母数字字符。`
invalid_group_team_map_error=`映射无效: %s`
@@ -603,26 +603,26 @@ captcha_incorrect=验证码不正确。
password_not_match=密码不匹配。
lang_select_error=从列表中选出语言
-username_been_taken=用户名已被使用。
+username_been_taken=用户名已使用。
username_change_not_local_user=非本地用户不允许更改用户名。
-change_username_disabled=更改用户名已被禁用。
+change_username_disabled=更改用户名已禁用。
change_full_name_disabled=更改用户全名已禁用
username_has_not_been_changed=用户名未更改
-repo_name_been_taken=仓库名称已被使用。
-repository_force_private=“强制私有”已启用:私有仓库不能被公开。
+repo_name_been_taken=仓库名称已使用。
+repository_force_private=「强制私有」已启用:私有仓库不能被公开。
repository_files_already_exist=此仓库已存在文件。请联系系统管理员。
repository_files_already_exist.adopt=此仓库已存在文件,只能被收录。
repository_files_already_exist.delete=此仓库已存在文件,必须先删除他们。
repository_files_already_exist.adopt_or_delete=此仓库已存在文件,要么删除他们,要么收录他们。
visit_rate_limit=远程访问达到速度限制。
2fa_auth_required=远程访问需要双重验证。
-org_name_been_taken=组织名称已被使用。
-team_name_been_taken=团队名称已被使用。
+org_name_been_taken=组织名称已使用。
+team_name_been_taken=团队名称已使用。
team_no_units_error=至少选择一项仓库单元。
-email_been_used=该电子邮件地址已在使用中。
+email_been_used=该邮箱地址已在使用中。
email_invalid=此邮箱地址无效。
email_domain_is_not_allowed=用户 %s 与EMAIL_DOMAIN_ALLOWLIT 或 EMAIL_DOMAIN_BLOCKLIT 冲突。请确保您的操作是预期的。
-openid_been_used=OpenID 地址 "%s" 已被使用。
+openid_been_used=OpenID 地址「%s」已被使用。
username_password_incorrect=用户名或密码不正确。
password_complexity=密码未达到复杂程度要求:
password_lowercase_one=至少一个小写字符
@@ -638,13 +638,13 @@ unsupported_login_type=此登录类型不支持手动删除帐户。
user_not_exist=该用户不存在
team_not_exist=团队不存在
last_org_owner=您不能从 "所有者" 团队中删除最后一个用户。组织中必须至少有一个所有者。
-cannot_add_org_to_team=组织不能被加入到团队中。
+cannot_add_org_to_team=组织不能加入到团队中。
duplicate_invite_to_team=此用户已被邀请为团队成员。
organization_leave_success=您已成功离开组织 %s。
-invalid_ssh_key=无法验证您的 SSH 密钥: %s
-invalid_gpg_key=无法验证您的 GPG 密钥: %s
-invalid_ssh_principal=无效的规则: %s
+invalid_ssh_key=无法验证您的 SSH 密钥:%s
+invalid_gpg_key=无法验证您的 GPG 密钥:%s
+invalid_ssh_principal=无效的规则:%s
must_use_public_key=您提供的密钥是私钥。不要在任何地方上传您的私钥,请改用您的公钥。
unable_verify_ssh_key=无法验证 SSH 密钥,请仔细检查是否有错误。
auth_failed=授权验证失败:%v
@@ -677,27 +677,27 @@ follow=关注
unfollow=取消关注
user_bio=简历
disabled_public_activity=该用户已隐藏活动记录。
-email_visibility.limited=所有已认证用户均可看到您的电子邮件地址
-email_visibility.private=只有你本人和管理员可以看到你的电子邮件地址
+email_visibility.limited=所有已认证用户均可看到您的邮箱地址
+email_visibility.private=只有您本人和管理员可以看到您的邮箱地址
show_on_map=在地图上显示这个位置
settings=用户设置
-form.name_reserved=用户名 "%s" 被保留。
-form.name_pattern_not_allowed=用户名中不允许使用 "%s" 格式。
-form.name_chars_not_allowed=用户名 "%s" 包含无效字符。
+form.name_reserved=用户名「%s」被保留。
+form.name_pattern_not_allowed=用户名中不允许使用「%s」格式。
+form.name_chars_not_allowed=用户名「%s」包含无效字符。
block.block=屏蔽
block.block.user=屏蔽用户
block.block.org=屏蔽用户访问组织
-block.block.failure=屏蔽用户失败: %s
+block.block.failure=屏蔽用户失败:%s
block.unblock=取消屏蔽
-block.unblock.failure=屏蔽用户失败: %s
+block.unblock.failure=取消屏蔽用户失败:%s
block.blocked=您已屏蔽此用户。
block.title=屏蔽一个用户
block.info=屏蔽用户会阻止他们与仓库进行交互,例如打开或评论合并请求或出现问题。了解更多关于屏蔽用户的信息。
block.info_1=阻止用户在您的帐户和仓库中进行以下操作:
-block.info_2=关注你的账号
-block.info_3=通过@提及您的用户名向您发送通知
+block.info_2=关注您的账号
+block.info_3=通过 @ 提及您的用户名向您发送通知
block.info_4=邀请您作为协作者到他们的仓库中
block.info_5=在仓库中点赞、派生或关注
block.info_6=打开和评论工单或合并请求
@@ -730,18 +730,18 @@ uid=UID
webauthn=两步验证(安全密钥)
public_profile=公开信息
-biography_placeholder=告诉我们一点您自己! (您可以使用Markdown)
-location_placeholder=与他人分享你的大概位置
-profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作
-password_username_disabled=您不被允许更改你的用户名。更多详情请联系您的系统管理员。
-password_full_name_disabled=您不被允许更改你的全名。请联系您的站点管理员了解更多详情。
+biography_placeholder=告诉我们一点您自己! (您可以使用 Markdown)
+location_placeholder=与他人分享您的大概位置
+profile_desc=控制您的个人资料对其他用户的显示方式。您的主邮箱地址将用于通知、密码恢复和基于网页的 Git 操作。
+password_username_disabled=您不被允许更改您的用户名。更多详情请联系您的系统管理员。
+password_full_name_disabled=您不被允许更改您的全名。请联系您的站点管理员了解更多详情。
full_name=自定义名称
website=个人网站
location=所在地区
update_theme=更新主题
update_profile=更新信息
update_language=更新语言
-update_language_not_found=语言 %s 不可用。
+update_language_not_found=语言「%s」不可用。
update_language_success=语言已更新。
update_profile_success=您的资料信息已经更新
change_username=您的用户名已更改。
@@ -752,7 +752,7 @@ cancel=取消操作
language=界面语言
ui=主题
hidden_comment_types=隐藏的评论类型
-hidden_comment_types_description=此处选中的注释类型不会显示在问题页面中。比如,勾选”标签“删除所有 " 添加/删除的