From 429ba9c010a06e9e89ad8b77b20350c093fd9447 Mon Sep 17 00:00:00 2001 From: Md Ferdous Alam Date: Tue, 24 Feb 2026 10:25:34 +0600 Subject: [PATCH 001/207] Use case-insensitive matching for Git error "Not a valid object name" (#36728) Fixes #36727 Git is lowercasing the `fatal: Not a valid object name` error message to follow its CodingGuidelines. This change makes the string matching case-insensitive so it works with both the current and future Git versions. --------- Co-authored-by: wxiaoguang --- modules/git/gitcmd/command_test.go | 13 +++++++++---- modules/git/gitcmd/error.go | 7 +++++++ modules/git/tree_nogogit.go | 2 +- routers/web/repo/pull.go | 6 +++--- services/wiki/wiki.go | 4 ++-- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/modules/git/gitcmd/command_test.go b/modules/git/gitcmd/command_test.go index 662356bc3f..6e4214d995 100644 --- a/modules/git/gitcmd/command_test.go +++ b/modules/git/gitcmd/command_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "strings" "testing" "time" @@ -49,9 +50,11 @@ func TestRunWithContextStd(t *testing.T) { stdout, stderr, err := cmd.RunStdString(t.Context()) if assert.Error(t, err) { assert.Equal(t, stderr, err.Stderr()) - assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr()) + stderrLower := strings.ToLower(stderr) // see: IsStdErrorNotValidObjectName + assert.Equal(t, "fatal: not a valid object name no-such\n", stderrLower) // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message - assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error()) + errLower := strings.ToLower(err.Error()) + assert.Equal(t, "exit status 128 - fatal: not a valid object name no-such", errLower) assert.Empty(t, stdout) } } @@ -61,9 +64,11 @@ func TestRunWithContextStd(t *testing.T) { stdout, stderr, err := cmd.RunStdBytes(t.Context()) if assert.Error(t, err) { assert.Equal(t, string(stderr), err.Stderr()) - assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr()) + stderrLower := strings.ToLower(err.Stderr()) // see: IsStdErrorNotValidObjectName + assert.Equal(t, "fatal: not a valid object name no-such\n", stderrLower) // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message - assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error()) + errLower := strings.ToLower(err.Error()) + assert.Equal(t, "exit status 128 - fatal: not a valid object name no-such", errLower) assert.Empty(t, stdout) } } diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index 066b37f10d..b674068c40 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -77,6 +77,13 @@ func IsErrorCanceledOrKilled(err error) bool { return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err) } +func IsStdErrorNotValidObjectName(err error) bool { + stderr, ok := ErrorAsStderr(err) + // Git is lowercasing the "fatal: Not a valid object name" error message + // ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com + return ok && strings.Contains(strings.ToLower(stderr), "fatal: not a valid object name") +} + type pipelineError struct { error } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index d50c1ad629..5d951dad0d 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -65,7 +65,7 @@ func (t *Tree) ListEntries() (Entries, error) { stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) if runErr != nil { - if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") { + if gitcmd.IsStdErrorNotValidObjectName(runErr) || strings.Contains(runErr.Error(), "fatal: not a tree object") { return nil, ErrNotExist{ ID: t.ID.String(), } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index d306927001..0578ab540f 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -282,7 +282,7 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") { + if gitcmd.IsStdErrorNotValidObjectName(err) || strings.Contains(err.Error(), "unknown revision or path not in the working tree") { ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 @@ -442,7 +442,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_s compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefName(pull.MergeBase), git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - if strings.Contains(err.Error(), "fatal: Not a valid object name") { + if gitcmd.IsStdErrorNotValidObjectName(err) { ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 @@ -584,7 +584,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_s compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - if strings.Contains(err.Error(), "fatal: Not a valid object name") { + if gitcmd.IsStdErrorNotValidObjectName(err) { ctx.Data["IsPullRequestBroken"] = true ctx.Data["BaseTarget"] = pull.BaseBranch ctx.Data["NumCommits"] = 0 diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index a025f26051..5bc62d451e 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "os" - "strings" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -16,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/graceful" @@ -59,7 +59,7 @@ func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath // Look for both files filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath) if err != nil { - if strings.Contains(err.Error(), "Not a valid object name") { + if gitcmd.IsStdErrorNotValidObjectName(err) { return false, gitPath, nil // branch doesn't exist } log.Error("Wiki LsTree failed, err: %v", err) From 75efc51e98e5753e4059b559e42537e9fb7858b0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Feb 2026 23:46:08 +0800 Subject: [PATCH 002/207] Fix incorrect setting loading order (#36735) --- modules/base/tool_test.go | 4 +++- modules/setting/security.go | 1 - modules/setting/setting.go | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index b7365e40c4..85d28836f1 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -29,8 +29,10 @@ func TestShortSha(t *testing.T) { func TestVerifyTimeLimitCode(t *testing.T) { defer test.MockVariableValue(&setting.InstallLock, true)() initGeneralSecret := func(secret string) { - setting.InstallLock = true setting.CfgProvider, _ = setting.NewConfigProviderFromData(fmt.Sprintf(` +[security] +INTERNAL_TOKEN = dummy +INSTALL_LOCK = true [oauth2] JWT_SECRET = %s `, secret)) diff --git a/modules/setting/security.go b/modules/setting/security.go index 743df61681..a1fd0bce2e 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -109,7 +109,6 @@ func generateSaveInternalToken(rootCfg ConfigProvider) { func loadSecurityFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("security") - InstallLock = HasInstallLock(rootCfg) LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31) SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY") if SecretKey == "" { diff --git a/modules/setting/setting.go b/modules/setting/setting.go index dc60d99bd6..f2b6274edc 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -108,6 +108,9 @@ func LoadCommonSettings() { // loadCommonSettingsFrom loads common configurations from a configuration provider. func loadCommonSettingsFrom(cfg ConfigProvider) error { + // a lot of logic depends on InstallLock value, so it must be loaded before any other settings + InstallLock = HasInstallLock(cfg) + // WARNING: don't change the sequence except you know what you are doing. loadRunModeFrom(cfg) loadLogGlobalFrom(cfg) From ed57c70176a6cac63f48ddf6b0d5f4f72cfea963 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 24 Feb 2026 12:22:04 -0800 Subject: [PATCH 003/207] Fix track time list permission check (#36662) Signed-off-by: Lunny Xiao Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang --- services/convert/issue.go | 15 +++++++++++++ services/convert/issue_test.go | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/services/convert/issue.go b/services/convert/issue.go index b396dd0737..fe4870b5db 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -13,6 +13,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -226,7 +227,21 @@ func ToStopWatches(ctx context.Context, doer *user_model.User, sws []*issues_mod // ToTrackedTimeList converts TrackedTimeList to API format func ToTrackedTimeList(ctx context.Context, doer *user_model.User, tl issues_model.TrackedTimeList) api.TrackedTimeList { result := make([]*api.TrackedTime, 0, len(tl)) + permCache := cache.NewEphemeralCache() for _, t := range tl { + // If the issue is not loaded, conservatively skip this entry to avoid bypassing permission checks. + if t.Issue == nil || t.Issue.Repo == nil { + continue + } + perm, err := cache.GetWithEphemeralCache(ctx, permCache, "repo-perm", t.Issue.RepoID, func(ctx context.Context, repoID int64) (access_model.Permission, error) { + return access_model.GetUserRepoPermission(ctx, t.Issue.Repo, doer) + }) + if err != nil { + continue + } + if !perm.CanReadIssuesOrPulls(t.Issue.IsPull) { + continue + } result = append(result, ToTrackedTime(ctx, doer, t)) } return result diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go index a12a69288a..109bf63e7d 100644 --- a/services/convert/issue_test.go +++ b/services/convert/issue_test.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLabel_ToLabel(t *testing.T) { @@ -83,3 +84,43 @@ func TestToStopWatchesRespectsPermissions(t *testing.T) { assert.Len(t, visibleAdmin, 2) assert.ElementsMatch(t, []string{"repo1", "repo3"}, []string{visibleAdmin[0].RepoName, visibleAdmin[1].RepoName}) } + +func TestToTrackedTime(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + ctx := t.Context() + publicIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1}) + privateIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 3}) + regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + publicTrackedTime := &issues_model.TrackedTime{IssueID: publicIssue.ID, UserID: regularUser.ID, Time: 3600} + privateTrackedTime := &issues_model.TrackedTime{IssueID: privateIssue.ID, UserID: regularUser.ID, Time: 1800} + require.NoError(t, db.Insert(ctx, publicTrackedTime)) + require.NoError(t, db.Insert(ctx, privateTrackedTime)) + + t.Run("NilIssues", func(t *testing.T) { + list := ToTrackedTimeList(ctx, regularUser, issues_model.TrackedTimeList{publicTrackedTime, privateTrackedTime}) + assert.Empty(t, list) + }) + + t.Run("NilRepo", func(t *testing.T) { + badTrackedTime := &issues_model.TrackedTime{Issue: &issues_model.Issue{RepoID: 999999}} + visible := ToTrackedTimeList(ctx, regularUser, issues_model.TrackedTimeList{badTrackedTime}) + assert.Empty(t, visible) + }) + + trackedTimes := issues_model.TrackedTimeList{publicTrackedTime, privateTrackedTime} + require.NoError(t, trackedTimes.LoadAttributes(ctx)) + + t.Run("ToRegularUser", func(t *testing.T) { + list := ToTrackedTimeList(ctx, regularUser, trackedTimes) + require.Len(t, list, 1) + assert.Equal(t, "repo1", list[0].Issue.Repo.Name) + }) + t.Run("ToAdminUser", func(t *testing.T) { + list := ToTrackedTimeList(ctx, adminUser, trackedTimes) + require.Len(t, list, 2) + assert.ElementsMatch(t, []string{"repo1", "repo3"}, []string{list[0].Issue.Repo.Name, list[1].Issue.Repo.Name}) + }) +} From d19d4da5ce81516bfedac2976f8f53f05badb629 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 25 Feb 2026 00:51:54 +0000 Subject: [PATCH 004/207] [skip ci] Updated translations via Crowdin --- options/locale/locale_fr-FR.json | 23 ++++++++++++++++++++--- options/locale/locale_ga-IE.json | 12 +++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/options/locale/locale_fr-FR.json b/options/locale/locale_fr-FR.json index 22be98d2ff..caa1b7dbf3 100644 --- a/options/locale/locale_fr-FR.json +++ b/options/locale/locale_fr-FR.json @@ -148,6 +148,13 @@ "filter.private": "Privé", "no_results_found": "Aucun résultat trouvé.", "internal_error_skipped": "Une erreur interne est survenue, mais ignorée : %s", + "characters_spaces": "Espaces", + "characters_tabs": "Tabulations", + "text_indent_style": "Style d’indentation", + "text_indent_size": "Taille de l’indentation", + "text_line_wrap": "Retour à la ligne", + "text_line_nowrap": "Pas de retour à la ligne", + "text_line_wrap_mode": "Mode de retour automatique à la ligne", "search.search": "Rechercher…", "search.type_tooltip": "Type de recherche", "search.fuzzy": "Approximative", @@ -751,6 +758,7 @@ "settings.add_email": "Ajouter un courriel", "settings.add_openid": "Ajouter une URI OpenID", "settings.add_email_confirmation_sent": "Un courriel de confirmation a été envoyé à « %s ». Veuillez vérifier votre boîte de réception dans les %s suivants pour confirmer votre adresse.", + "settings.email_primary_not_found": "L’adresse courriel sélectionnée est introuvable.", "settings.add_email_success": "La nouvelle adresse a été ajoutée.", "settings.email_preference_set_success": "Le courriel de préférence a été défini avec succès.", "settings.add_openid_success": "La nouvelle adresse OpenID a été ajoutée.", @@ -1490,6 +1498,7 @@ "repo.issues.filter_sort.feweststars": "Favoris (croissant)", "repo.issues.filter_sort.mostforks": "Bifurcations (décroissant)", "repo.issues.filter_sort.fewestforks": "Bifurcations (croissant)", + "repo.issues.quick_goto": "Allez au ticket", "repo.issues.action_open": "Ouvrir", "repo.issues.action_close": "Fermer", "repo.issues.action_label": "Label", @@ -1777,6 +1786,8 @@ "repo.pulls.title_desc": "souhaite fusionner %[1]d révision(s) depuis %[2]s vers %[3]s", "repo.pulls.merged_title_desc": "a fusionné %[1]d révision(s) à partir de %[2]s vers %[3]s %[4]s", "repo.pulls.change_target_branch_at": "a remplacée la branche cible %s par %s %s.", + "repo.pulls.marked_as_work_in_progress_at": "a marqué la demande d’ajout comme travail en cours %s", + "repo.pulls.marked_as_ready_for_review_at": "a marqué la demande d’ajout comme prête pour relecture %s", "repo.pulls.tab_conversation": "Discussion", "repo.pulls.tab_commits": "Révisions", "repo.pulls.tab_files": "Fichiers Modifiés", @@ -1795,6 +1806,7 @@ "repo.pulls.remove_prefix": "Enlever le préfixe %s", "repo.pulls.data_broken": "Cette demande d’ajout est impossible par manque d'informations de bifurcation.", "repo.pulls.files_conflicted": "Cette demande d'ajout contient des modifications en conflit avec la branche ciblée.", + "repo.pulls.files_conflicted_no_listed_files": "(Aucun fichier en conflit répertorié)", "repo.pulls.is_checking": "Recherche de conflits de fusion…", "repo.pulls.is_ancestor": "Cette branche est déjà présente dans la branche ciblée. Il n'y a rien à fusionner.", "repo.pulls.is_empty": "Les changements sur cette branche sont déjà sur la branche cible. Cette révision sera vide.", @@ -1865,6 +1877,7 @@ "repo.pulls.update_not_allowed": "Vous n'êtes pas autorisé à mettre à jour la branche", "repo.pulls.outdated_with_base_branch": "Cette branche est désynchronisée avec la branche de base", "repo.pulls.close": "Fermer la demande d’ajout", + "repo.pulls.reopen": "Rouvrir la demande d’ajout", "repo.pulls.closed_at": "a fermé cette demande d'ajout %[2]s.", "repo.pulls.reopened_at": "a rouvert cette demande d'ajout %[2]s.", "repo.pulls.cmd_instruction_hint": "Voir les instructions en ligne de commande", @@ -2120,6 +2133,8 @@ "repo.settings.pulls.ignore_whitespace": "Ignorer les espaces lors des conflits", "repo.settings.pulls.enable_autodetect_manual_merge": "Activer la détection automatique de la fusion manuelle (Remarque : dans certains cas particuliers, des erreurs de détection peuvent se produire)", "repo.settings.pulls.allow_rebase_update": "Activer la mise à jour de demande d'ajout par rebase", + "repo.settings.pulls.default_target_branch": "Branche cible par défaut pour les nouvelles demandes d’ajout", + "repo.settings.pulls.default_target_branch_default": "Branche par défaut (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "Supprimer la branche après la fusion par default", "repo.settings.pulls.default_allow_edits_from_maintainers": "Autoriser les modifications par les mainteneurs par défaut", "repo.settings.releases_desc": "Activer les publications du dépôt", @@ -2432,7 +2447,8 @@ "repo.settings.block_outdated_branch_desc": "La fusion ne sera pas possible lorsque la branche principale est derrière la branche de base.", "repo.settings.block_admin_merge_override": "Les administrateurs doivent respecter les règles de protection des branches", "repo.settings.block_admin_merge_override_desc": "Les administrateurs doivent respecter les règles de protection des branches et ne peuvent pas les contourner.", - "repo.settings.default_branch_desc": "Sélectionnez une branche par défaut pour les demandes de fusion et les révisions :", + "repo.settings.default_branch_desc": "Sélectionnez une branche par défaut pour les révisions.", + "repo.settings.default_target_branch_desc": "Les demandes d’ajout peuvent utiliser une branche cible différente, telle que définie dans la section Demandes d’ajouts des Paramètres avancés du dépôt.", "repo.settings.merge_style_desc": "Styles de fusion", "repo.settings.default_merge_style_desc": "Méthode de fusion par défaut", "repo.settings.choose_branch": "Choisissez une branche…", @@ -2646,7 +2662,7 @@ "repo.branch.restore_success": "La branche \"%s\" a été restaurée.", "repo.branch.restore_failed": "Impossible de restaurer la branche \"%s\".", "repo.branch.protected_deletion_failed": "La branche \"%s\" est protégé. Elle ne peut pas être supprimée.", - "repo.branch.default_deletion_failed": "La branche \"%s\" est la branche par défaut. Elle ne peut pas être supprimée.", + "repo.branch.default_deletion_failed": "« %s » est la branche par défaut ou la cible de demandes d’ajout. Elle ne peut pas être supprimée.", "repo.branch.default_branch_not_exist": "La branche par défaut « %s » n‘existe pas.", "repo.branch.restore": "Restaurer la branche \"%s\"", "repo.branch.download": "Télécharger la branche \"%s\"", @@ -2663,7 +2679,7 @@ "repo.branch.new_branch_from": "Créer une nouvelle branche à partir de \"%s\"", "repo.branch.renamed": "La branche %s à été renommée en %s.", "repo.branch.rename_default_or_protected_branch_error": "Seuls les administrateurs peuvent renommer les branches par défaut ou protégées.", - "repo.branch.rename_protected_branch_failed": "Cette branche est protégée par des règles de protection basées sur des globs.", + "repo.branch.rename_protected_branch_failed": "Impossible de renommer cette branche en raison des règles de protection de branche.", "repo.branch.commits_divergence_from": "Divergence de révisions : %[1]d en retard et %[2]d en avance sur %[3]s", "repo.branch.commits_no_divergence": "Identique à la branche %[1]s", "repo.tag.create_tag": "Créer l'étiquette %s", @@ -3679,6 +3695,7 @@ "actions.runs.delete.description": "Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée.", "actions.runs.not_done": "Cette exécution du flux de travail n’est pas terminée.", "actions.runs.view_workflow_file": "Voir le fichier du flux de travail", + "actions.runs.workflow_graph": "Graphique du flux", "actions.workflow.disable": "Désactiver le flux de travail", "actions.workflow.disable_success": "Le flux de travail « %s » a bien été désactivé.", "actions.workflow.enable": "Activer le flux de travail", diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 14123c5002..ad00325b02 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -148,6 +148,13 @@ "filter.private": "Príobháideach", "no_results_found": "Níor aimsíodh aon torthaí.", "internal_error_skipped": "Tharla earráid inmheánach ach éirithe as: %s", + "characters_spaces": "Spásanna", + "characters_tabs": "Cluaisíní", + "text_indent_style": "Stíl eangaithe", + "text_indent_size": "Méid an línithe", + "text_line_wrap": "Fillte", + "text_line_nowrap": "Gan fillte", + "text_line_wrap_mode": "Mód fillte líne", "search.search": "Cuardaigh…", "search.type_tooltip": "Cineál cuardaigh", "search.fuzzy": "Doiléir", @@ -751,6 +758,7 @@ "settings.add_email": "Cuir Seoladh R-phoist leis", "settings.add_openid": "Cuir OpenID URI", "settings.add_email_confirmation_sent": "Seoladh ríomhphost deimhnithe chuig “%s”. Seiceáil do bhosca isteach laistigh den chéad %s eile chun do sheoladh ríomhphoist a dhearbhú.", + "settings.email_primary_not_found": "Níorbh fhéidir an seoladh ríomhphoist roghnaithe a aimsiú.", "settings.add_email_success": "Cuireadh an seoladh ríomhphoist nua leis.", "settings.email_preference_set_success": "Socraíodh rogha ríomhphoist go rathúil.", "settings.add_openid_success": "Cuireadh an seoladh OpenID nua leis.", @@ -1525,7 +1533,7 @@ "repo.issues.comment_pull_merged_at": "cumasc tiomantas %[1]s le %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "cumasc tiomantas %[1]s le %[2]s %[3]s", "repo.issues.close_comment_issue": "Dún le trácht", - "repo.issues.reopen_issue": "Athoscail", + "repo.issues.reopen_issue": "Athoscail an Cheist", "repo.issues.reopen_comment_issue": "Athoscail le trácht", "repo.issues.create_comment": "Trácht", "repo.issues.comment.blocked_user": "Ní féidir trácht a chruthú nó a chur in eagar toisc go bhfuil an tráchtaire nó úinéir an stórais bac ort.", @@ -1869,6 +1877,7 @@ "repo.pulls.update_not_allowed": "Ní cheadaítear duit brainse a nuashonrú", "repo.pulls.outdated_with_base_branch": "Tá an brainse seo as dáta leis an mbunbhrainse", "repo.pulls.close": "Dún Iarratas Tarraing", + "repo.pulls.reopen": "Athoscail Iarratas Tarraingthe", "repo.pulls.closed_at": "dhún an t-iarratas tarraingthe seo %[2]s", "repo.pulls.reopened_at": "athoscail an t-iarratas tarraingthe seo %[2]s", "repo.pulls.cmd_instruction_hint": "Féach ar threoracha na n-orduithe", @@ -3686,6 +3695,7 @@ "actions.runs.delete.description": "An bhfuil tú cinnte gur mian leat an rith sreabha oibre seo a scriosadh go buan? Ní féidir an gníomh seo a chealú.", "actions.runs.not_done": "Níl an rith sreabha oibre seo críochnaithe.", "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", + "actions.runs.workflow_graph": "Graf Sreabhadh Oibre", "actions.workflow.disable": "Díchumasaigh sreabhadh oibre", "actions.workflow.disable_success": "D'éirigh le sreabhadh oibre '%s' a dhíchumasú.", "actions.workflow.enable": "Cumasaigh sreabhadh oibre", From 2176e84ab977011ff2bc3f3a9066020cc674f6b1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 25 Feb 2026 09:21:07 +0800 Subject: [PATCH 005/207] Fix path resolving (#36734) --- modules/util/path.go | 78 +++++++++++++++++++++++--- modules/util/path_test.go | 68 +++++++++++++++++++++++ services/repository/generate.go | 82 +++++++++++++--------------- services/repository/generate_test.go | 79 ++++++++++++++++++--------- 4 files changed, 228 insertions(+), 79 deletions(-) diff --git a/modules/util/path.go b/modules/util/path.go index 0cb8ab7ece..48447d7b90 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -64,7 +64,7 @@ func PathJoinRelX(elem ...string) string { return PathJoinRel(elems...) } -const pathSeparator = string(os.PathSeparator) +const filepathSeparator = string(os.PathSeparator) // FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately. // All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators. @@ -82,7 +82,7 @@ func FilePathJoinAbs(base string, sub ...string) string { if isOSWindows() { elems[0] = filepath.Clean(base) } else { - elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator)) + elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", filepathSeparator)) } if !filepath.IsAbs(elems[0]) { // This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead @@ -93,9 +93,9 @@ func FilePathJoinAbs(base string, sub ...string) string { continue } if isOSWindows() { - elems = append(elems, filepath.Clean(pathSeparator+s)) + elems = append(elems, filepath.Clean(filepathSeparator+s)) } else { - elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator))) + elems = append(elems, filepath.Clean(filepathSeparator+strings.ReplaceAll(s, "\\", filepathSeparator))) } } // the elems[0] must be an absolute path, just join them together @@ -115,12 +115,72 @@ func IsDir(dir string) (bool, error) { return false, err } -func IsRegularFile(filePath string) (bool, error) { - f, err := os.Lstat(filePath) - if err == nil { - return f.Mode().IsRegular(), nil +var ErrNotRegularPathFile = errors.New("not a regular file") + +// ReadRegularPathFile reads a file with given sub path in root dir. +// It returns error when the path is not a regular file, or any parent path is not a regular directory. +func ReadRegularPathFile(root, filePathIn string, limit int) ([]byte, error) { + pathFields := strings.Split(PathJoinRelX(filePathIn), "/") + + targetPathBuilder := strings.Builder{} + targetPathBuilder.Grow(len(root) + len(filePathIn) + 2) + targetPathBuilder.WriteString(root) + targetPathString := root + for i, subPath := range pathFields { + targetPathBuilder.WriteByte(filepath.Separator) + targetPathBuilder.WriteString(subPath) + targetPathString = targetPathBuilder.String() + + expectFile := i == len(pathFields)-1 + st, err := os.Lstat(targetPathString) + if err != nil { + return nil, err + } + if expectFile && !st.Mode().IsRegular() || !expectFile && !st.Mode().IsDir() { + return nil, fmt.Errorf("%w: %s", ErrNotRegularPathFile, filePathIn) + } } - return false, err + f, err := os.Open(targetPathString) + if err != nil { + return nil, err + } + defer f.Close() + return ReadWithLimit(f, limit) +} + +// WriteRegularPathFile writes data to a file with given sub path in root dir, it creates parent directories if necessary. +// The file is created with fileMode, and the directories are created with dirMode. +// It returns error when the path already exists but is not a regular file, or any parent path is not a regular directory. +func WriteRegularPathFile(root, filePathIn string, data []byte, dirMode, fileMode os.FileMode) error { + pathFields := strings.Split(PathJoinRelX(filePathIn), "/") + + targetPathBuilder := strings.Builder{} + targetPathBuilder.Grow(len(root) + len(filePathIn) + 2) + targetPathBuilder.WriteString(root) + targetPathString := root + for i, subPath := range pathFields { + targetPathBuilder.WriteByte(filepath.Separator) + targetPathBuilder.WriteString(subPath) + targetPathString = targetPathBuilder.String() + + expectFile := i == len(pathFields)-1 + st, err := os.Lstat(targetPathString) + if err == nil { + if expectFile && !st.Mode().IsRegular() || !expectFile && !st.Mode().IsDir() { + return fmt.Errorf("%w: %s", ErrNotRegularPathFile, filePathIn) + } + continue + } + if !os.IsNotExist(err) { + return err + } + if !expectFile { + if err = os.Mkdir(targetPathString, dirMode); err != nil { + return err + } + } + } + return os.WriteFile(targetPathString, data, fileMode) } // IsExist checks whether a file or directory exists. diff --git a/modules/util/path_test.go b/modules/util/path_test.go index 79c37e55f7..2469088b3a 100644 --- a/modules/util/path_test.go +++ b/modules/util/path_test.go @@ -6,6 +6,7 @@ package util import ( "net/url" "os" + "path/filepath" "runtime" "testing" @@ -230,3 +231,70 @@ func TestListDirRecursively(t *testing.T) { require.NoError(t, err) assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res) } + +func TestReadWriteRegularPathFile(t *testing.T) { + const readLimit = 10000 + tmpDir := t.TempDir() + rootDir := tmpDir + "/root" + _ = os.Mkdir(rootDir, 0o755) + _ = os.WriteFile(tmpDir+"/other-file", []byte("other-content"), 0o755) + _ = os.Mkdir(rootDir+"/real-dir", 0o755) + _ = os.WriteFile(rootDir+"/real-dir/real-file", []byte("dummy-content"), 0o644) + _ = os.Symlink(rootDir+"/real-dir", rootDir+"/link-dir") + _ = os.Symlink(rootDir+"/real-dir/real-file", rootDir+"/real-dir/link-file") + + t.Run("Read", func(t *testing.T) { + content, err := os.ReadFile(filepath.Join(rootDir, "../other-file")) + require.NoError(t, err) + assert.Equal(t, "other-content", string(content)) + + content, err = ReadRegularPathFile(rootDir, "../other-file", readLimit) + require.ErrorIs(t, err, os.ErrNotExist) + assert.Empty(t, string(content)) + + content, err = ReadRegularPathFile(rootDir, "real-dir/real-file", readLimit) + require.NoError(t, err) + assert.Equal(t, "dummy-content", string(content)) + + _, err = ReadRegularPathFile(rootDir, "link-dir/real-file", readLimit) + require.ErrorIs(t, err, ErrNotRegularPathFile) + _, err = ReadRegularPathFile(rootDir, "real-dir/link-file", readLimit) + require.ErrorIs(t, err, ErrNotRegularPathFile) + _, err = ReadRegularPathFile(rootDir, "link-dir/link-file", readLimit) + require.ErrorIs(t, err, ErrNotRegularPathFile) + }) + + t.Run("Write", func(t *testing.T) { + assertFileContent := func(path, expected string) { + data, err := os.ReadFile(path) + if expected == "" { + assert.ErrorIs(t, err, os.ErrNotExist) + return + } + require.NoError(t, err) + assert.Equal(t, expected, string(data), "file content mismatch for %s", path) + } + + err := WriteRegularPathFile(rootDir, "new-dir/new-file", []byte("new-content"), 0o755, 0o644) + require.NoError(t, err) + assertFileContent(rootDir+"/new-dir/new-file", "new-content") + + err = WriteRegularPathFile(rootDir, "link-dir/real-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + err = WriteRegularPathFile(rootDir, "link-dir/link-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + err = WriteRegularPathFile(rootDir, "link-dir/new-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + err = WriteRegularPathFile(rootDir, "real-dir/link-file", []byte("new-content"), 0o755, 0o644) + require.ErrorIs(t, err, ErrNotRegularPathFile) + + err = WriteRegularPathFile(rootDir, "../other-file", []byte("new-content"), 0o755, 0o644) + require.NoError(t, err) + assertFileContent(rootDir+"/../other-file", "other-content") + assertFileContent(rootDir+"/other-file", "new-content") + + err = WriteRegularPathFile(rootDir, "real-dir/real-file", []byte("changed-content"), 0o755, 0o644) + require.NoError(t, err) + assertFileContent(rootDir+"/real-dir/real-file", "changed-content") + }) +} diff --git a/services/repository/generate.go b/services/repository/generate.go index bc37bc7bfe..83e9c22e54 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -103,12 +103,12 @@ func generateExpansion(ctx context.Context, src string, templateRepo, generateRe // giteaTemplateFileMatcher holds information about a .gitea/template file type giteaTemplateFileMatcher struct { - LocalFullPath string - globs []glob.Glob + relPath string + globs []glob.Glob } -func newGiteaTemplateFileMatcher(fullPath string, content []byte) *giteaTemplateFileMatcher { - gt := &giteaTemplateFileMatcher{LocalFullPath: fullPath} +func newGiteaTemplateFileMatcher(relPath string, content []byte) *giteaTemplateFileMatcher { + gt := &giteaTemplateFileMatcher{relPath: relPath} gt.globs = make([]glob.Glob, 0) scanner := bufio.NewScanner(bytes.NewReader(content)) for scanner.Scan() { @@ -139,64 +139,44 @@ func (gt *giteaTemplateFileMatcher) Match(s string) bool { return false } -func readLocalTmpRepoFileContent(localPath string, limit int) ([]byte, error) { - ok, err := util.IsRegularFile(localPath) - if err != nil { - return nil, err - } else if !ok { - return nil, fs.ErrNotExist - } - - f, err := os.Open(localPath) - if err != nil { - return nil, err - } - defer f.Close() - - return util.ReadWithLimit(f, limit) -} - func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) { - localPath := filepath.Join(tmpDir, ".gitea", "template") - content, err := readLocalTmpRepoFileContent(localPath, 1024*1024) + templateRelPath := filepath.Join(".gitea", "template") + content, err := util.ReadRegularPathFile(tmpDir, templateRelPath, 1024*1024) if err != nil { - return nil, err + return nil, util.Iif(errors.Is(err, util.ErrNotRegularPathFile), os.ErrNotExist, err) } - return newGiteaTemplateFileMatcher(localPath, content), nil + return newGiteaTemplateFileMatcher(templateRelPath, content), nil } func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, templateRepo, generateRepo *repo_model.Repository) error { - tmpFullPath := filepath.Join(tmpDir, tmpDirSubPath) - content, err := readLocalTmpRepoFileContent(tmpFullPath, 1024*1024) + content, err := util.ReadRegularPathFile(tmpDir, tmpDirSubPath, 1024*1024) if err != nil { - return util.Iif(errors.Is(err, fs.ErrNotExist), nil, err) + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return err } - if err := util.Remove(tmpFullPath); err != nil { + if err := os.Remove(util.FilePathJoinAbs(tmpDir, tmpDirSubPath)); err != nil { return err } generatedContent := generateExpansion(ctx, string(content), templateRepo, generateRepo) substSubPath := filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo)) - newLocalPath := filepath.Join(tmpDir, substSubPath) - regular, err := util.IsRegularFile(newLocalPath) - if canWrite := regular || errors.Is(err, fs.ErrNotExist); !canWrite { - return nil - } - if err := os.MkdirAll(filepath.Dir(newLocalPath), 0o755); err != nil { - return err - } - return os.WriteFile(newLocalPath, []byte(generatedContent), 0o644) + return util.WriteRegularPathFile(tmpDir, substSubPath, []byte(generatedContent), 0o755, 0o644) } -func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, fileMatcher *giteaTemplateFileMatcher) error { - if err := util.Remove(fileMatcher.LocalFullPath); err != nil { - return fmt.Errorf("unable to remove .gitea/template: %w", err) +// processGiteaTemplateFile processes and removes the .gitea/template file, does variable expansion for template files +// and save the processed files to the filesystem. It returns a list of skipped files that are not regular paths. +func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, fileMatcher *giteaTemplateFileMatcher) (skippedFiles []string, _ error) { + // Why not use "os.Root" here: symlink is unsafe even in the same root but "os.Root" can't help, it's more difficult to use "os.Root" to do the WalkDir. + if err := os.Remove(util.FilePathJoinAbs(tmpDir, fileMatcher.relPath)); err != nil { + return nil, fmt.Errorf("unable to remove .gitea/template: %w", err) } if !fileMatcher.HasRules() { - return nil // Avoid walking tree if there are no globs + return skippedFiles, nil // Avoid walking tree if there are no globs } - return filepath.WalkDir(tmpDir, func(fullPath string, d os.DirEntry, walkErr error) error { + err := filepath.WalkDir(tmpDir, func(fullPath string, d os.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } @@ -208,10 +188,22 @@ func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, return err } if fileMatcher.Match(filepath.ToSlash(tmpDirSubPath)) { - return substGiteaTemplateFile(ctx, tmpDir, tmpDirSubPath, templateRepo, generateRepo) + err := substGiteaTemplateFile(ctx, tmpDir, tmpDirSubPath, templateRepo, generateRepo) + if errors.Is(err, util.ErrNotRegularPathFile) { + skippedFiles = append(skippedFiles, tmpDirSubPath) + } else if err != nil { + return err + } } return nil }) // end: WalkDir + if err != nil { + return nil, err + } + if err = util.RemoveAll(util.FilePathJoinAbs(tmpDir, ".git")); err != nil { + return nil, err + } + return skippedFiles, nil } func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error { @@ -236,7 +228,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r // Variable expansion fileMatcher, err := readGiteaTemplateFile(tmpDir) if err == nil { - err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher) + _, err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher) if err != nil { return fmt.Errorf("processGiteaTemplateFile: %w", err) } diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go index 432de4dc59..160dbb9a06 100644 --- a/services/repository/generate_test.go +++ b/services/repository/generate_test.go @@ -74,7 +74,7 @@ func TestFilePathSanitize(t *testing.T) { assert.Equal(t, ".", filePathSanitize("/")) } -func TestProcessGiteaTemplateFile(t *testing.T) { +func TestProcessGiteaTemplateFileGenerate(t *testing.T) { tmpDir := filepath.Join(t.TempDir(), "gitea-template-test") assertFileContent := func(path, expected string) { @@ -97,6 +97,8 @@ func TestProcessGiteaTemplateFile(t *testing.T) { assert.Equal(t, expected, link, "symlink target mismatch for %s", path) } + require.NoError(t, os.MkdirAll(tmpDir+"/.git", 0o755)) + require.NoError(t, os.WriteFile(tmpDir+"/.git/config", []byte("git-config-dummy"), 0o644)) require.NoError(t, os.MkdirAll(tmpDir+"/.gitea", 0o755)) require.NoError(t, os.WriteFile(tmpDir+"/.gitea/template", []byte("*\ninclude/**"), 0o644)) require.NoError(t, os.MkdirAll(tmpDir+"/sub", 0o755)) @@ -127,10 +129,20 @@ func TestProcessGiteaTemplateFile(t *testing.T) { assertFileContent("subst-${TEMPLATE_NAME}-to-link", toLinkContent) assertFileContent("subst-${TEMPLATE_NAME}-from-link", fromLinkContent) } + + // case-5 + { + require.NoError(t, os.MkdirAll(tmpDir+"/real-dir", 0o755)) + require.NoError(t, os.WriteFile(tmpDir+"/real-dir/real-file", []byte("origin content"), 0o644)) + require.NoError(t, os.MkdirAll(tmpDir+"/include/subst-${TEMPLATE_NAME}-link-dir", 0o755)) + require.NoError(t, os.WriteFile(tmpDir+"/include/subst-${TEMPLATE_NAME}-link-dir/real-file", []byte("template content"), 0o644)) + require.NoError(t, os.Symlink(tmpDir+"/real-dir", tmpDir+"/include/subst-TemplateRepoName-link-dir")) + } + { // will succeed require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-normal", []byte("dummy subst template name normal"), 0o644)) - // will skil if the path subst result is a link + // will be skipped if the path subst result is a link require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-to-link", []byte("dummy subst template name to link"), 0o644)) require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/subst-TemplateRepoName-to-link")) // will be skipped since the source is a symlink @@ -143,9 +155,20 @@ func TestProcessGiteaTemplateFile(t *testing.T) { { templateRepo := &repo_model.Repository{Name: "TemplateRepoName"} generatedRepo := &repo_model.Repository{Name: "/../.gIt/name"} + assertFileContent(".git/config", "git-config-dummy") fileMatcher, _ := readGiteaTemplateFile(tmpDir) - err := processGiteaTemplateFile(t.Context(), tmpDir, templateRepo, generatedRepo, fileMatcher) + skippedFiles, err := processGiteaTemplateFile(t.Context(), tmpDir, templateRepo, generatedRepo, fileMatcher) require.NoError(t, err) + assert.Equal(t, []string{ + "include/subst-${TEMPLATE_NAME}-link-dir/real-file", + "include/subst-TemplateRepoName-link-dir", + "link", + "subst-${TEMPLATE_NAME}-from-link", + "subst-${TEMPLATE_NAME}-to-link", + "subst-TemplateRepoName-to-link", + }, skippedFiles) + assertFileContent(".git/config", "") + assertFileContent(".gitea/template", "") assertFileContent("include/foo/bar/test.txt", "include subdir TemplateRepoName") } @@ -182,32 +205,38 @@ func TestProcessGiteaTemplateFile(t *testing.T) { assertSymLink("subst-${TEMPLATE_NAME}-from-link", tmpDir+"/sub/link-target") } + // case-5 { - templateFilePath := tmpDir + "/.gitea/template" - - _ = os.Remove(templateFilePath) - _, err := os.Lstat(templateFilePath) - require.ErrorIs(t, err, fs.ErrNotExist) - _, err = readGiteaTemplateFile(tmpDir) // no template file - require.ErrorIs(t, err, fs.ErrNotExist) - - _ = os.WriteFile(templateFilePath+".target", []byte("test-data-target"), 0o644) - _ = os.Symlink(templateFilePath+".target", templateFilePath) - content, _ := os.ReadFile(templateFilePath) - require.Equal(t, "test-data-target", string(content)) - _, err = readGiteaTemplateFile(tmpDir) // symlinked template file - require.ErrorIs(t, err, fs.ErrNotExist) - - _ = os.Remove(templateFilePath) - _ = os.WriteFile(templateFilePath, []byte("test-data-regular"), 0o644) - content, _ = os.ReadFile(templateFilePath) - require.Equal(t, "test-data-regular", string(content)) - fm, err := readGiteaTemplateFile(tmpDir) // regular template file - require.NoError(t, err) - assert.Len(t, fm.globs, 1) + assertFileContent("real-dir/real-file", "origin content") } } +func TestProcessGiteaTemplateFileRead(t *testing.T) { + tmpDir := t.TempDir() + _ = os.Mkdir(tmpDir+"/.gitea", 0o755) + templateFilePath := tmpDir + "/.gitea/template" + _ = os.Remove(templateFilePath) + _, err := os.Lstat(templateFilePath) + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = readGiteaTemplateFile(tmpDir) // no template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.WriteFile(templateFilePath+".target", []byte("test-data-target"), 0o644) + _ = os.Symlink(templateFilePath+".target", templateFilePath) + content, _ := os.ReadFile(templateFilePath) + require.Equal(t, "test-data-target", string(content)) + _, err = readGiteaTemplateFile(tmpDir) // symlinked template file + require.ErrorIs(t, err, fs.ErrNotExist) + + _ = os.Remove(templateFilePath) + _ = os.WriteFile(templateFilePath, []byte("test-data-regular"), 0o644) + content, _ = os.ReadFile(templateFilePath) + require.Equal(t, "test-data-regular", string(content)) + fm, err := readGiteaTemplateFile(tmpDir) // regular template file + require.NoError(t, err) + assert.Len(t, fm.globs, 1) +} + func TestTransformers(t *testing.T) { cases := []struct { name string From 577ed107ddb7c054c0d55e5c5e2027d015116ee0 Mon Sep 17 00:00:00 2001 From: Viktor Suprun Date: Thu, 26 Feb 2026 01:54:02 +1100 Subject: [PATCH 006/207] Fix SVG height calculation in diff viewer (#36748) Fixes #36742 --- web_src/js/features/imagediff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/imagediff.ts b/web_src/js/features/imagediff.ts index 23f05fbdc7..1e89aa8b6e 100644 --- a/web_src/js/features/imagediff.ts +++ b/web_src/js/features/imagediff.ts @@ -53,7 +53,7 @@ function getDefaultSvgBoundsIfUndefined(text: string, src: string): Bounds | nul const viewBox = svg.viewBox.baseVal; return { width: defaultSize, - height: defaultSize * viewBox.width / viewBox.height, + height: defaultSize * viewBox.height / viewBox.width, }; } return { From 569c49debe06f30a2bbb50b3812e705c556b8adf Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 25 Feb 2026 08:28:39 -0800 Subject: [PATCH 007/207] Add validation constraints for repository creation fields (#36671) Adds validation constraints to repository creation inputs, enforcing max-length limits for labels/license/readme and enum validation for trust model and object format. Updates both the API option struct and the web form struct to keep validation consistent. --- modules/structs/repo.go | 8 ++++---- services/forms/repo_form.go | 8 ++++---- services/repository/create.go | 3 +++ templates/swagger/v1_json.tmpl | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 765546a5aa..a08cf36037 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -135,7 +135,7 @@ type CreateRepoOption struct { // Whether the repository is private Private bool `json:"private"` // Label-Set to use - IssueLabels string `json:"issue_labels"` + IssueLabels string `json:"issue_labels" binding:"MaxSize(255)"` // Whether the repository should be auto-initialized? AutoInit bool `json:"auto_init"` // Whether the repository is template @@ -143,15 +143,15 @@ type CreateRepoOption struct { // Gitignores to use Gitignores string `json:"gitignores"` // License to use - License string `json:"license"` + License string `json:"license" binding:"MaxSize(100)"` // Readme of the repository to create - Readme string `json:"readme"` + Readme string `json:"readme" binding:"MaxSize(255)"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` // TrustModel of the repository // enum: default,collaborator,committer,collaboratorcommitter TrustModel string `json:"trust_model"` - // ObjectFormatName of the underlying git repository + // ObjectFormatName of the underlying git repository, empty string for default (sha1) // enum: sha1,sha256 ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"` } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 765a723968..8b69c6bcc6 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -27,9 +27,9 @@ type CreateRepoForm struct { DefaultBranch string `binding:"GitRefName;MaxSize(100)"` AutoInit bool Gitignores string - IssueLabels string - License string - Readme string + IssueLabels string `binding:"MaxSize(255)"` + License string `binding:"MaxSize(100)"` + Readme string `binding:"MaxSize(255)"` Template bool RepoTemplate int64 @@ -41,7 +41,7 @@ type CreateRepoForm struct { Labels bool ProtectedBranch bool - ForkSingleBranch string + ForkSingleBranch string `binding:"MaxSize(255)"` ObjectFormatName string } diff --git a/services/repository/create.go b/services/repository/create.go index cbdc9cca76..e027d3b979 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -230,6 +230,9 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User, if opts.ObjectFormatName == "" { opts.ObjectFormatName = git.Sha1ObjectFormat.Name() } + if opts.ObjectFormatName != git.Sha1ObjectFormat.Name() && opts.ObjectFormatName != git.Sha256ObjectFormat.Name() { + return nil, fmt.Errorf("unsupported object format: %s", opts.ObjectFormatName) + } repo := &repo_model.Repository{ OwnerID: owner.ID, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 570747ca57..a1ecc7fb4f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -23780,7 +23780,7 @@ "x-go-name": "Name" }, "object_format_name": { - "description": "ObjectFormatName of the underlying git repository", + "description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)", "type": "string", "enum": [ "sha1", From 0de8a3d3d8346d04c32934de554cb2ab66fbc294 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 25 Feb 2026 21:08:08 +0100 Subject: [PATCH 008/207] Avoid opening new tab when downloading actions logs (#36740) `target="_blank"` causes the browser to flash a new tab when actions logs are downloaded. Using the [`download`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#download) attribute fixes this. --- web_src/js/components/RepoActionView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index efa3472e8c..4a8da7b11d 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -626,7 +626,7 @@ export default defineComponent({
- + {{ locale.downloadLogs }} From 9ae28b6f3985992f4e761e96c215fc21cfa2773a Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 25 Feb 2026 21:20:28 +0100 Subject: [PATCH 009/207] Change image transparency grid to CSS (#36711) These new colors work much better on dark theme than before (where it was far too bright). image image --------- Co-authored-by: Giteabot Co-authored-by: Claude Opus 4.6 --- web_src/css/base.css | 2 +- web_src/css/themes/theme-gitea-dark.css | 2 ++ web_src/css/themes/theme-gitea-light.css | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index 5a75aaaee6..3fa5c1246c 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -42,7 +42,7 @@ --gap-inline: 0.25rem; /* gap for inline texts and elements, for example: the spaces for sentence with labels, button text, etc */ --gap-block: 0.5rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */ - --background-view-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAG0lEQVQYlWN4+vTpf3SMDTAMBYXYBLFpHgoKAeiOf0SGE9kbAAAAAElFTkSuQmCC") right bottom var(--color-primary-light-7); + --background-view-image: repeating-conic-gradient(var(--color-transparency-grid-dark) 0 25%, var(--color-transparency-grid-light) 0 50%) 0 0 / 18px 18px; } @media (min-width: 768px) and (max-width: 1200px) { diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index f58c222c9a..ad5eec9e82 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -247,6 +247,8 @@ gitea-theme-meta-info { --color-highlight-bg: #352c1c; --color-overlay-backdrop: #080808c0; --color-danger: var(--color-red); + --color-transparency-grid-light: #2a2a2a; + --color-transparency-grid-dark: #1a1a1a; accent-color: var(--color-accent); color-scheme: dark; } diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 8766bf7abc..049b64f73f 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -247,6 +247,8 @@ gitea-theme-meta-info { --color-highlight-bg: #fffbdd; --color-overlay-backdrop: #080808c0; --color-danger: var(--color-red); + --color-transparency-grid-light: #fafafa; + --color-transparency-grid-dark: #e2e2e2; accent-color: var(--color-accent); color-scheme: light; } From 840cf68c3e7d8cb7dcd5e2038935139a3d16241d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 26 Feb 2026 04:59:29 +0800 Subject: [PATCH 010/207] Fix release draft access check logic (#36720) 1. remove hasRepoWriteScope to avoid abuse 2. clarify "ctx.Written" behavior 3. merge "read-only" tests to slightly improve performance --- routers/api/v1/repo/release.go | 42 +++++++------------ routers/api/v1/repo/release_attachment.go | 20 +++------ .../api_releases_attachment_test.go | 9 +--- tests/integration/api_releases_test.go | 32 +++++++------- 4 files changed, 38 insertions(+), 65 deletions(-) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 4f17590abd..ff43628fa5 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -10,7 +10,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" @@ -22,26 +21,19 @@ import ( release_service "code.gitea.io/gitea/services/release" ) -func hasRepoWriteScope(ctx *context.APIContext) bool { - scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ctx.Data["IsApiToken"] != true || !ok { - return true - } - - requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository) - allow, err := scope.HasScope(requiredScopes...) - if err != nil { - ctx.APIError(http.StatusForbidden, "checking scope failed: "+err.Error()) - return false - } - return allow -} - -func canAccessDraftRelease(ctx *context.APIContext) bool { +func canAccessReleaseDraft(ctx *context.APIContext) bool { if !ctx.IsSigned || !ctx.Repo.CanWrite(unit.TypeReleases) { return false } - return hasRepoWriteScope(ctx) + if ctx.Data["IsApiToken"] != true { + // not API token request, the request is from a user session with write access + return true + } + // the request is from an access token with scope + scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository) + allow, _ := scope.HasScope(requiredScopes...) // err (invalid token) can be safely ignored + return allow } // GetRelease get a single release of a repository @@ -85,13 +77,9 @@ func GetRelease(ctx *context.APIContext) { return } - if release.IsDraft { // only the users with write access can see draft releases - if !canAccessDraftRelease(ctx) { - if !ctx.Written() { - ctx.APIErrorNotFound() - } - return - } + if release.IsDraft && !canAccessReleaseDraft(ctx) { // only the users with write access can see draft releases + ctx.APIErrorNotFound() + return } if err := release.LoadAttributes(ctx); err != nil { @@ -182,14 +170,12 @@ func ListReleases(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" listOptions := utils.GetListOptions(ctx) - - includeDrafts := (ctx.Repo.AccessMode >= perm.AccessModeWrite || ctx.Repo.UnitAccessMode(unit.TypeReleases) >= perm.AccessModeWrite) && hasRepoWriteScope(ctx) if ctx.Written() { return } opts := repo_model.FindReleasesOptions{ ListOptions: listOptions, - IncludeDrafts: includeDrafts, + IncludeDrafts: canAccessReleaseDraft(ctx), IncludeTags: false, IsDraft: ctx.FormOptionalBool("draft"), IsPreRelease: ctx.FormOptionalBool("pre-release"), diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 6b30070db8..19075961f3 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -34,13 +34,9 @@ func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool { ctx.APIErrorNotFound() return false } - if release.IsDraft { - if !canAccessDraftRelease(ctx) { - if !ctx.Written() { - ctx.APIErrorNotFound() - } - return false - } + if release.IsDraft && !canAccessReleaseDraft(ctx) { + ctx.APIErrorNotFound() + return false } return true } @@ -149,13 +145,9 @@ func ListReleaseAttachments(ctx *context.APIContext) { ctx.APIErrorNotFound() return } - if release.IsDraft { - if !canAccessDraftRelease(ctx) { - if !ctx.Written() { - ctx.APIErrorNotFound() - } - return - } + if release.IsDraft && !canAccessReleaseDraft(ctx) { + ctx.APIErrorNotFound() + return } if err := release.LoadAttributes(ctx); err != nil { ctx.APIErrorInternal(err) diff --git a/tests/integration/api_releases_attachment_test.go b/tests/integration/api_releases_attachment_test.go index e859b23c72..3f2592e331 100644 --- a/tests/integration/api_releases_attachment_test.go +++ b/tests/integration/api_releases_attachment_test.go @@ -15,16 +15,13 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { +func testAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { // Limit the allowed release types (since by default there is no restriction) defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".exe")() - defer tests.PrepareTestEnv(t)() - attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9}) release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) @@ -42,9 +39,7 @@ func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) { session.MakeRequest(t, req, http.StatusUnprocessableEntity) } -func TestAPIDraftReleaseAttachmentAccess(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIDraftReleaseAttachmentAccess(t *testing.T) { attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 13}) release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID}) diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index c7200129dd..c7f1343dde 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -29,9 +29,19 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAPIListReleasesWithWriteToken(t *testing.T) { +func TestAPIReleaseRead(t *testing.T) { defer tests.PrepareTestEnv(t)() + t.Run("DraftReleaseAttachmentAccess", testAPIDraftReleaseAttachmentAccess) + t.Run("ListReleasesWithWriteToken", testAPIListReleasesWithWriteToken) + t.Run("ListReleasesWithReadToken", testAPIListReleasesWithReadToken) + t.Run("GetDraftRelease", testAPIGetDraftRelease) + t.Run("GetLatestRelease", testAPIGetLatestRelease) + t.Run("GetReleaseByTag", testAPIGetReleaseByTag) + t.Run("GetDraftReleaseByTag", testAPIGetDraftReleaseByTag) + t.Run("EditReleaseAttachmentWithUnallowedFile", testAPIEditReleaseAttachmentWithUnallowedFile) // failed attempt, so it is also a read test +} +func testAPIListReleasesWithWriteToken(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository) @@ -81,9 +91,7 @@ func TestAPIListReleasesWithWriteToken(t *testing.T) { testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft") } -func TestAPIListReleasesWithReadToken(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIListReleasesWithReadToken(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository) @@ -129,9 +137,7 @@ func TestAPIListReleasesWithReadToken(t *testing.T) { testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft") } -func TestAPIGetDraftRelease(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetDraftRelease(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 4}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -300,9 +306,7 @@ func TestAPICreateReleaseGivenInvalidTarget(t *testing.T) { MakeRequest(t, req, http.StatusNotFound) } -func TestAPIGetLatestRelease(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetLatestRelease(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -315,9 +319,7 @@ func TestAPIGetLatestRelease(t *testing.T) { assert.Equal(t, "testing-release", release.Title) } -func TestAPIGetReleaseByTag(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetReleaseByTag(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -341,9 +343,7 @@ func TestAPIGetReleaseByTag(t *testing.T) { assert.NotEmpty(t, err.Message) } -func TestAPIGetDraftReleaseByTag(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetDraftReleaseByTag(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) From 0d006290a7b8c96e544d4d73b8bf7a5b2047bbf4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Feb 2026 11:50:44 +0100 Subject: [PATCH 011/207] Inline and lazy-load EasyMDE CSS, fix border colors (#36714) Replace the external easymde.min.css import with an inlined and lazy-loaded CSS file that uses proper theme variables for border colors. All EasyMDE/CodeMirror rules are scoped under `.EasyMDEContainer`, removing the need for !important overrides. - Fixes easymde borders, these were broken since a while now - Scope all easymde styles to .EasyMDEContainer - Inline easymde.min.css and codemirror.css into web_src/css/easymde.css - Lazy-load the CSS alongside the JS in switchToEasyMDE() - Fix .editor-toolbar and .CodeMirror border colors to use --color-input-border matching textarea inputs - Remove unused gutter, line number, and other unconfigured styles - Move .editor-loading to codeeditor.css where it belongs image --------- Signed-off-by: silverwind Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web_src/css/codemirror/base.css | 49 -- web_src/css/codemirror/dark.css | 88 ++-- web_src/css/easymde.css | 442 ++++++++++++++++++ web_src/css/editor/fileeditor.css | 56 --- web_src/css/features/codeeditor.css | 5 + web_src/css/index.css | 2 - .../js/features/comp/ComboMarkdownEditor.ts | 6 +- web_src/js/index-domready.ts | 1 - 8 files changed, 495 insertions(+), 154 deletions(-) delete mode 100644 web_src/css/codemirror/base.css create mode 100644 web_src/css/easymde.css delete mode 100644 web_src/css/editor/fileeditor.css diff --git a/web_src/css/codemirror/base.css b/web_src/css/codemirror/base.css deleted file mode 100644 index aedf7d8560..0000000000 --- a/web_src/css/codemirror/base.css +++ /dev/null @@ -1,49 +0,0 @@ -.ui .field:not(:last-child) .EasyMDEContainer .editor-statusbar { - margin-bottom: -1em; /* when there is a statusbar, the "margin-bottom: 1em" of the "field" is not needed, because the statusbar is likely a blank line */ -} - -.EasyMDEContainer .CodeMirror { - color: var(--color-input-text); - background-color: var(--color-input-background); - border-color: var(--color-secondary); - font: 14px var(--fonts-monospace); -} - -.EasyMDEContainer .CodeMirror.cm-s-default { - border-radius: var(--border-radius); - padding: 0 !important; -} - -.EasyMDEContainer .CodeMirror.CodeMirror-fullscreen.CodeMirror-focused { - border-right: 1px solid var(--color-primary) !important; -} - -.CodeMirror-cursor { - border-color: var(--color-caret) !important; -} - -.CodeMirror .cm-comment { - background: inherit !important; -} - -.CodeMirror .CodeMirror-code { - font: 14px var(--fonts-monospace); -} - -.CodeMirror-selected { - background: var(--color-primary-light-1) !important; - color: var(--color-white) !important; -} - -.CodeMirror-placeholder { - color: var(--color-placeholder-text) !important; - opacity: 1 !important; -} - -.CodeMirror-focused { - border-color: var(--color-primary) !important; -} - -.CodeMirror :focus { - outline: none; -} diff --git a/web_src/css/codemirror/dark.css b/web_src/css/codemirror/dark.css index 8a20d1c004..0fcc13c076 100644 --- a/web_src/css/codemirror/dark.css +++ b/web_src/css/codemirror/dark.css @@ -1,106 +1,106 @@ -.CodeMirror.cm-s-default .cm-property, -.CodeMirror.cm-s-paper .cm-property { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-property, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-property { color: #a0cc75; } -.CodeMirror.cm-s-default .cm-header, -.CodeMirror.cm-s-paper .cm-header { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-header, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-header { color: #9daccc; } -.CodeMirror.cm-s-default .cm-quote, -.CodeMirror.cm-s-paper .cm-quote { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-quote, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-quote { color: #009900; } -.CodeMirror.cm-s-default .cm-keyword, -.CodeMirror.cm-s-paper .cm-keyword { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-keyword, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-keyword { color: #cc8a61; } -.CodeMirror.cm-s-default .cm-atom, -.CodeMirror.cm-s-paper .cm-atom { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-atom, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-atom { color: #ef5e77; } -.CodeMirror.cm-s-default .cm-number, -.CodeMirror.cm-s-paper .cm-number { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-number, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-number { color: #ff5656; } -.CodeMirror.cm-s-default .cm-def, -.CodeMirror.cm-s-paper .cm-def { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-def, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-def { color: #e4e4e4; } -.CodeMirror.cm-s-default .cm-variable-2, -.CodeMirror.cm-s-paper .cm-variable-2 { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-variable-2, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-variable-2 { color: #00bdbf; } -.CodeMirror.cm-s-default .cm-variable-3, -.CodeMirror.cm-s-paper .cm-variable-3 { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-variable-3, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-variable-3 { color: #008855; } -.CodeMirror.cm-s-default .cm-comment, -.CodeMirror.cm-s-paper .cm-comment { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-comment, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-comment { color: #8e9ab3; } -.CodeMirror.cm-s-default .cm-string, -.CodeMirror.cm-s-paper .cm-string { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-string, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-string { color: #a77272; } -.CodeMirror.cm-s-default .cm-string-2, -.CodeMirror.cm-s-paper .cm-string-2 { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-string-2, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-string-2 { color: #ff5500; } -.CodeMirror.cm-s-default .cm-meta, -.CodeMirror.cm-s-paper .cm-meta, -.CodeMirror.cm-s-default .cm-qualifier, -.CodeMirror.cm-s-paper .cm-qualifier { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-meta, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-meta, +.EasyMDEContainer .CodeMirror.cm-s-default .cm-qualifier, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-qualifier { color: #ffb176; } -.CodeMirror.cm-s-default .cm-builtin, -.CodeMirror.cm-s-paper .cm-builtin { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-builtin, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-builtin { color: #b7c951; } -.CodeMirror.cm-s-default .cm-bracket, -.CodeMirror.cm-s-paper .cm-bracket { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-bracket, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-bracket { color: #999977; } -.CodeMirror.cm-s-default .cm-tag, -.CodeMirror.cm-s-paper .cm-tag { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-tag, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-tag { color: #f1d273; } -.CodeMirror.cm-s-default .cm-attribute, -.CodeMirror.cm-s-paper .cm-attribute { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-attribute, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-attribute { color: #bfcc70; } -.CodeMirror.cm-s-default .cm-hr, -.CodeMirror.cm-s-paper .cm-hr { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-hr, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-hr { color: #999999; } -.CodeMirror.cm-s-default .cm-url, -.CodeMirror.cm-s-paper .cm-url { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-url, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-url { color: #c5cfd0; } -.CodeMirror.cm-s-default .cm-link, -.CodeMirror.cm-s-paper .cm-link { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-link, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-link { color: #d8c792; } -.CodeMirror.cm-s-default .cm-error, -.CodeMirror.cm-s-paper .cm-error { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-error, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-error { color: #dbdbeb; } diff --git a/web_src/css/easymde.css b/web_src/css/easymde.css new file mode 100644 index 0000000000..a3193bcfbe --- /dev/null +++ b/web_src/css/easymde.css @@ -0,0 +1,442 @@ +/* Inlined styles from easymde.min.css (includes EasyMDE and CodeMirror base) */ +.EasyMDEContainer { + display: block; +} + +/* CodeMirror base layout (from codemirror.css) */ +.EasyMDEContainer .CodeMirror { + position: relative; + overflow: hidden; + box-sizing: border-box; + height: auto; + border: 1px solid var(--color-input-border); + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); + padding: 10px; + font: 14px var(--fonts-monospace); + z-index: 0; + overflow-wrap: break-word; + color: var(--color-input-text); + background-color: var(--color-input-background); + direction: ltr; +} + +.EasyMDEContainer .CodeMirror.cm-s-default { + border-radius: var(--border-radius); + padding: 0; +} + +.EasyMDEContainer .CodeMirror-lines { + padding: 4px 0; + cursor: text; + min-height: 1px; +} + +.EasyMDEContainer .CodeMirror pre.CodeMirror-line, +.EasyMDEContainer .CodeMirror pre.CodeMirror-line-like { + padding: 0 4px; + border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + overflow-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + font-variant-ligatures: contextual; +} + +.EasyMDEContainer .CodeMirror-wrap pre.CodeMirror-line, +.EasyMDEContainer .CodeMirror-wrap pre.CodeMirror-line-like { + overflow-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.EasyMDEContainer .CodeMirror-scroll { + overflow: scroll !important; /* things will break if this is overridden */ + margin-bottom: -50px; + margin-right: -50px; + padding-bottom: 50px; + height: 100%; + outline: none; + position: relative; + z-index: 0; + cursor: text; +} + +.EasyMDEContainer .CodeMirror-sizer { + position: relative; + border-right: 50px solid transparent; +} + +.EasyMDEContainer .CodeMirror-vscrollbar, +.EasyMDEContainer .CodeMirror-hscrollbar, +.EasyMDEContainer .CodeMirror-scrollbar-filler, +.EasyMDEContainer .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; + outline: none; +} + +.EasyMDEContainer .CodeMirror-vscrollbar { + right: 0; + top: 0; + overflow-x: hidden; + overflow-y: scroll; +} + +.EasyMDEContainer .CodeMirror-hscrollbar { + bottom: 0; + left: 0; + overflow-y: hidden; + overflow-x: scroll; +} + +.EasyMDEContainer .CodeMirror-scrollbar-filler { + right: 0; + bottom: 0; +} + +/* Cursor */ +.EasyMDEContainer .CodeMirror-cursor { + position: absolute; + pointer-events: none; + border-left: 1px solid var(--color-caret); + border-right: none; + width: 0; +} + +.EasyMDEContainer div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} + +.EasyMDEContainer div.CodeMirror-dragcursors { + visibility: visible; +} + +.EasyMDEContainer .CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +/* Selection */ +.EasyMDEContainer .CodeMirror-selected { + background: var(--color-primary-light-1); +} + +.EasyMDEContainer .CodeMirror-focused .CodeMirror-selected { + background: var(--color-primary-light-1); +} + +.EasyMDEContainer .CodeMirror-line::selection, +.EasyMDEContainer .CodeMirror-line > span::selection, +.EasyMDEContainer .CodeMirror-line > span > span::selection { + background: var(--color-primary-light-1); +} + +/* Misc */ +.EasyMDEContainer .cm-tab { + display: inline-block; + text-decoration: inherit; +} + +.EasyMDEContainer .CodeMirror-rtl pre { + direction: rtl; +} + +.EasyMDEContainer .CodeMirror-code { + font: 14px var(--fonts-monospace); + outline: none; +} + +.EasyMDEContainer .CodeMirror-scroll, +.EasyMDEContainer .CodeMirror-sizer { + box-sizing: content-box; +} + +.EasyMDEContainer .CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.EasyMDEContainer .CodeMirror-measure pre { + position: static; +} + +.EasyMDEContainer .CodeMirror-composing { + border-bottom: 2px solid; +} + +.EasyMDEContainer span.CodeMirror-selectedtext { + background: none; +} + +@media print { + .EasyMDEContainer .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* Default theme overrides */ +.EasyMDEContainer .cm-header, +.EasyMDEContainer .cm-strong { + font-weight: var(--font-weight-bold); +} + +.EasyMDEContainer .cm-em { + font-style: italic; +} + +.EasyMDEContainer .cm-link { + text-decoration: underline; +} + +.EasyMDEContainer .cm-strikethrough { + text-decoration: line-through; +} + +.EasyMDEContainer .cm-comment { + background: inherit; +} + +/* Placeholder */ +.EasyMDEContainer .CodeMirror-placeholder { + color: var(--color-placeholder-text); + opacity: 1; +} + +/* Focus */ +.EasyMDEContainer .CodeMirror-focused { + border-color: var(--color-primary); +} + +.EasyMDEContainer .CodeMirror :focus { + outline: none; +} + +/* Fullscreen */ +.EasyMDEContainer .CodeMirror-fullscreen { + background: var(--color-body); + position: fixed; + inset: 50px 0 0; + height: auto; + z-index: 8; + border-right: none; + border-bottom-right-radius: 0; +} + +.EasyMDEContainer .CodeMirror-fullscreen.CodeMirror-focused { + border-right: 1px solid var(--color-primary); +} + +/* Statusbar */ +.ui .field:not(:last-child) .EasyMDEContainer .editor-statusbar { + margin-bottom: -1em; /* when there is a statusbar, the "margin-bottom: 1em" of the "field" is not needed, because the statusbar is likely a blank line */ +} + +/* Toolbar */ +.EasyMDEContainer .editor-toolbar { + position: relative; + user-select: none; + padding: 9px 10px; + border-top: 1px solid var(--color-input-border); + border-left: 1px solid var(--color-input-border); + border-right: 1px solid var(--color-input-border); + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); +} + +.EasyMDEContainer .editor-toolbar.fullscreen { + width: 100%; + height: 50px; + padding-top: 10px; + padding-bottom: 10px; + box-sizing: border-box; + background: var(--color-body); + border: 0; + position: fixed; + top: 0; + left: 0; + opacity: 1; + z-index: 9; +} + +.EasyMDEContainer .editor-toolbar button { + background: transparent; + display: inline-block; + text-align: center; + text-decoration: none; + height: 30px; + margin: 0; + padding: 0 6px; + border: none; + border-radius: 3px; + cursor: pointer; + font-weight: var(--font-weight-bold); + min-width: 30px; + white-space: nowrap; + color: var(--color-text-light); +} + +.EasyMDEContainer .editor-toolbar button:not(:hover) { + background-color: transparent; +} + +.EasyMDEContainer .editor-toolbar button:hover { + background: var(--color-hover); +} + +.EasyMDEContainer .editor-toolbar button.active { + background: var(--color-active); +} + +.EasyMDEContainer .editor-toolbar i.separator { + display: inline-block; + width: 0; + border-left: none; + border-right: 1px solid var(--color-input-border); + color: transparent; + text-indent: -10px; + margin: 0 6px; +} + +.EasyMDEContainer .editor-toolbar button::after { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + font-size: 65%; + vertical-align: text-bottom; + position: relative; + top: 2px; +} + +.EasyMDEContainer .editor-toolbar button.heading-1::after { + content: "1"; +} + +.EasyMDEContainer .editor-toolbar button.heading-2::after { + content: "2"; +} + +.EasyMDEContainer .editor-toolbar button.heading-3::after { + content: "3"; +} + +.EasyMDEContainer .editor-toolbar button.heading-bigger::after { + content: "\25B2"; +} + +.EasyMDEContainer .editor-toolbar button.heading-smaller::after { + content: "\25BC"; +} + +.EasyMDEContainer .editor-toolbar.disabled-for-preview button:not(.no-disable) { + opacity: 0.6; + pointer-events: none; +} + +/* hide preview button, we have the preview tab for this */ +.EasyMDEContainer .editor-toolbar:not(.fullscreen) .preview { + display: none; +} + +/* hide revert button in fullscreen, it breaks the page */ +.EasyMDEContainer .editor-toolbar.fullscreen .revert-to-textarea { + display: none; +} + +@media only screen and (max-width: 700px) { + .EasyMDEContainer .editor-toolbar i.no-mobile { + display: none; + } +} + +/* Statusbar */ +.EasyMDEContainer .editor-statusbar { + padding: 8px 10px; + font-size: 12px; + color: var(--color-text-light); + text-align: right; +} + +.EasyMDEContainer .editor-statusbar span { + display: inline-block; + min-width: 4em; + margin-left: 1em; +} + +.EasyMDEContainer .editor-statusbar .lines::before { + content: "lines: "; +} + +.EasyMDEContainer .editor-statusbar .words::before { + content: "words: "; +} + +.EasyMDEContainer .editor-statusbar .characters::before { + content: "characters: "; +} + +/* Preview */ +.EasyMDEContainer .editor-preview-full { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 7; + overflow: auto; + display: none; + box-sizing: border-box; +} + +.EasyMDEContainer .editor-preview-side { + position: fixed; + bottom: 0; + width: 50%; + top: 50px; + right: 0; + z-index: 9; + overflow: auto; + display: none; + box-sizing: border-box; + border: 1px solid var(--color-secondary); + overflow-wrap: break-word; +} + +.EasyMDEContainer .editor-preview-active-side { + display: block; +} + +.EasyMDEContainer .editor-preview-active { + display: block; +} + +.EasyMDEContainer .editor-preview { + padding: 10px; + background-color: var(--color-body); +} + +.EasyMDEContainer .editor-preview > p { + margin-top: 0; +} + +.EasyMDEContainer .editor-preview pre { + background: var(--color-markup-code-block); + margin-bottom: 10px; +} + +.EasyMDEContainer .editor-preview table td, +.EasyMDEContainer .editor-preview table th { + border: 1px solid var(--color-secondary); + padding: 5px; +} diff --git a/web_src/css/editor/fileeditor.css b/web_src/css/editor/fileeditor.css deleted file mode 100644 index 12ae97a109..0000000000 --- a/web_src/css/editor/fileeditor.css +++ /dev/null @@ -1,56 +0,0 @@ -.editor-toolbar { - border-color: var(--color-secondary); -} - -.editor-toolbar.fullscreen { - background: var(--color-body); -} - -.editor-toolbar button { - border: none !important; - color: var(--color-text-light); -} - -.editor-toolbar button:not(:hover) { - background-color: transparent !important; -} - -.editor-toolbar i.separator { - border-left: none; - border-right-color: var(--color-secondary); -} - -.editor-toolbar button:hover { - background: var(--color-hover); -} - -.editor-toolbar button.active { - background: var(--color-active); -} - -/* hide preview button, we have the preview tab for this */ -.editor-toolbar:not(.fullscreen) .preview { - display: none; -} - -/* hide revert button in fullscreen, it breaks the page */ -.editor-toolbar.fullscreen .revert-to-textarea { - display: none; -} - -.editor-preview { - background-color: var(--color-body); -} - -.editor-preview-side { - border-color: var(--color-secondary); -} - -.editor-statusbar { - color: var(--color-text-light); -} - -.editor-loading { - padding: 1rem; - text-align: center; -} diff --git a/web_src/css/features/codeeditor.css b/web_src/css/features/codeeditor.css index 8df3429b09..33a9191f40 100644 --- a/web_src/css/features/codeeditor.css +++ b/web_src/css/features/codeeditor.css @@ -1,3 +1,8 @@ +.editor-loading { + padding: 1rem; + text-align: center; +} + .monaco-editor-container, .editor-loading.is-loading { width: 100%; diff --git a/web_src/css/index.css b/web_src/css/index.css index c02651d520..699ba221ca 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -52,7 +52,6 @@ @import "./markup/asciicast.css"; @import "./chroma/base.css"; -@import "./codemirror/base.css"; @import "./font_i18n.css"; @import "./base.css"; @import "./home.css"; @@ -74,7 +73,6 @@ @import "./repo/commit-sign.css"; @import "./repo/packages.css"; -@import "./editor/fileeditor.css"; @import "./editor/combomarkdowneditor.css"; @import "./org.css"; diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index fdc8a1d601..42104947df 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -318,8 +318,10 @@ export class ComboMarkdownEditor { async switchToEasyMDE() { if (this.easyMDE) return; - // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles. - const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde'); + const [{default: EasyMDE}] = await Promise.all([ + import(/* webpackChunkName: "easymde" */'easymde'), + import(/* webpackChunkName: "easymde" */'../../../css/easymde.css'), + ]); const easyMDEOpt: EasyMDE.Options = { autoDownloadFontAwesome: false, element: this.textarea, diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index 187876df44..fb445b8df4 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -1,5 +1,4 @@ import '../fomantic/build/fomantic.js'; -import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in "switchToEasyMDE" import {initHtmx} from './htmx.ts'; import {initDashboardRepoList} from './features/dashboard.ts'; From d0f92cb0a133c325323121ad391fbf043e3a6edb Mon Sep 17 00:00:00 2001 From: danigm Date: Thu, 26 Feb 2026 12:56:02 +0100 Subject: [PATCH 012/207] Add created_by filter to SearchIssues (#36670) This patch adds the created_by filter to the SearchIssues method. tea cli has an option to filter by author when listing issues, but it's not working. The tea command line creates this request for the API when using the author filter: ``` $ tea issue list -l local --kind pull -A danigm -vvv http://localhost:3000/api/v1/repos/issues/search?created_by=danigm&labels=&limit=30&milestones=&page=1&state=open&type=pulls ``` This patch fixes the API to allow this kind of queries from go-sdk and tea cli. --------- Co-authored-by: wxiaoguang Co-authored-by: silverwind --- routers/api/v1/repo/issue.go | 12 ++++++++++++ templates/swagger/v1_json.tmpl | 6 ++++++ tests/integration/api_issue_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 41076fd99c..22324e1923 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -157,6 +157,10 @@ func SearchIssues(ctx *context.APIContext) { // in: query // description: Filter by repository owner // type: string + // - name: created_by + // in: query + // description: Only show items which were created by the given user + // type: string // - name: team // in: query // description: Filter by team (requires organization owner parameter) @@ -257,6 +261,14 @@ func SearchIssues(ctx *context.APIContext) { searchOpt.UpdatedBeforeUnix = optional.Some(before) } + createdByID := getUserIDForFilter(ctx, "created_by") + if ctx.Written() { + return + } + if createdByID > 0 { + searchOpt.PosterID = strconv.FormatInt(createdByID, 10) + } + if ctx.IsSigned { ctxUserID := ctx.Doer.ID if ctx.FormBool("created") { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a1ecc7fb4f..7b86cc3d45 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4300,6 +4300,12 @@ "name": "owner", "in": "query" }, + { + "type": "string", + "description": "Only show items which were created by the given user", + "name": "created_by", + "in": "query" + }, { "type": "string", "description": "Filter by team (requires organization owner parameter)", diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 56bed7db0d..8d85543dc8 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -361,6 +361,34 @@ func TestAPISearchIssues(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) + + query = url.Values{"created": {"1"}} // issues created by the auth user + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 5) + + query = url.Values{"created": {"1"}, "type": {"pulls"}} // prs created by the auth user + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 3) + + query = url.Values{"created_by": {"user2"}} // issues created by the user2 + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 9) + + query = url.Values{"created_by": {"user2"}, "type": {"pulls"}} // prs created by user2 + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 3) } func TestAPISearchIssuesWithLabels(t *testing.T) { From 26d83c932a8cc6f6f984a76d6b57945f99664cb1 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 26 Feb 2026 16:16:11 +0100 Subject: [PATCH 013/207] Instance-wide (global) info banner and maintenance mode (#36571) The banner allows site operators to communicate important announcements (e.g., maintenance windows, policy updates, service notices) directly within the UI. The maintenance mode only allows admin to access the web UI. * Fix #2345 * Fix #9618 --------- Co-authored-by: wxiaoguang --- modules/markup/sanitizer_default.go | 2 +- modules/setting/config.go | 26 ++- modules/setting/config/value.go | 163 +++++++++---- modules/setting/config_option_instance.go | 58 +++++ modules/web/middleware/cookie.go | 6 +- options/locale/locale_en-US.json | 8 + routers/common/errpage.go | 5 +- routers/common/maintenancemode.go | 43 ++++ routers/init.go | 1 + routers/private/internal.go | 2 + routers/web/admin/config.go | 78 ++----- routers/web/auth/auth.go | 5 + routers/web/misc/misc.go | 9 + routers/web/misc/webtheme.go | 2 +- routers/web/repo/view_home.go | 3 - routers/web/web.go | 2 +- services/context/context.go | 10 +- services/context/context_template.go | 25 +- .../config_settings/config_settings.tmpl | 8 +- templates/admin/config_settings/instance.tmpl | 63 ++++++ .../admin/config_settings/repository.tmpl | 15 +- templates/admin/layout_head.tmpl | 2 +- templates/base/head_banner.tmpl | 11 + templates/base/head_navbar.tmpl | 1 + templates/shared/combomarkdowneditor.tmpl | 8 +- tests/integration/admin_config_test.go | 46 ++++ tests/integration/config_instance_test.go | 126 +++++++++++ web_src/css/admin.css | 8 + web_src/css/modules/container.css | 18 ++ web_src/js/features/admin/config.test.ts | 41 ++++ web_src/js/features/admin/config.ts | 214 ++++++++++++++++-- web_src/js/features/common-fetch-action.ts | 13 +- .../js/features/comp/ComboMarkdownEditor.ts | 4 +- web_src/js/features/repo-editor.ts | 2 +- 34 files changed, 870 insertions(+), 158 deletions(-) create mode 100644 modules/setting/config_option_instance.go create mode 100644 routers/common/maintenancemode.go create mode 100644 templates/admin/config_settings/instance.tmpl create mode 100644 templates/base/head_banner.tmpl create mode 100644 tests/integration/config_instance_test.go create mode 100644 web_src/js/features/admin/config.test.ts diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 7fdf66c4bc..77ba8bf4f4 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -81,7 +81,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { "data-markdown-generated-content", "data-attr-class", } generalSafeElements := []string{ - "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt", + "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "center", "i", "strong", "em", "a", "pre", "code", "img", "tt", "div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label", "dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary", "details", "caption", "figure", "figcaption", diff --git a/modules/setting/config.go b/modules/setting/config.go index fb99325a95..bde8e4ac2a 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -12,8 +12,8 @@ import ( ) type PictureStruct struct { - DisableGravatar *config.Value[bool] - EnableFederatedAvatar *config.Value[bool] + DisableGravatar *config.Option[bool] + EnableFederatedAvatar *config.Option[bool] } type OpenWithEditorApp struct { @@ -23,6 +23,9 @@ type OpenWithEditorApp struct { type OpenWithEditorAppsType []OpenWithEditorApp +// ToTextareaString is only used in templates, for help prompt only +// TODO: OPEN-WITH-EDITOR-APP-JSON: Because there is no "rich UI", a plain text editor is used to manage the list of apps +// Maybe we can use some better formats like Yaml in the future, then a simple textarea can manage the config clearly func (t OpenWithEditorAppsType) ToTextareaString() string { var ret strings.Builder for _, app := range t { @@ -31,7 +34,7 @@ func (t OpenWithEditorAppsType) ToTextareaString() string { return ret.String() } -func DefaultOpenWithEditorApps() OpenWithEditorAppsType { +func openWithEditorAppsDefaultValue() OpenWithEditorAppsType { return OpenWithEditorAppsType{ { DisplayName: "VS Code", @@ -49,13 +52,14 @@ func DefaultOpenWithEditorApps() OpenWithEditorAppsType { } type RepositoryStruct struct { - OpenWithEditorApps *config.Value[OpenWithEditorAppsType] - GitGuideRemoteName *config.Value[string] + OpenWithEditorApps *config.Option[OpenWithEditorAppsType] + GitGuideRemoteName *config.Option[string] } type ConfigStruct struct { Picture *PictureStruct Repository *RepositoryStruct + Instance *InstanceStruct } var ( @@ -67,12 +71,16 @@ func initDefaultConfig() { config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) defaultConfig = &ConfigStruct{ Picture: &PictureStruct{ - DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), - EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), + DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), + EnableFederatedAvatar: config.NewOption[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), }, Repository: &RepositoryStruct{ - OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"), - GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"), + OpenWithEditorApps: config.NewOption[OpenWithEditorAppsType]("repository.open-with.editor-apps").WithEmptyAsDefault().WithDefaultFunc(openWithEditorAppsDefaultValue), + GitGuideRemoteName: config.NewOption[string]("repository.git-guide-remote-name").WithEmptyAsDefault().WithDefaultSimple("origin"), + }, + Instance: &InstanceStruct{ + WebBanner: config.NewOption[WebBannerType]("instance.web_banner"), + MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"), }, } } diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go index 301c60f5e8..bd91add97a 100644 --- a/modules/setting/config/value.go +++ b/modules/setting/config/value.go @@ -5,6 +5,7 @@ package config import ( "context" + "reflect" "sync" "code.gitea.io/gitea/modules/json" @@ -16,18 +17,31 @@ type CfgSecKey struct { Sec, Key string } -type Value[T any] struct { +// OptionInterface is used to overcome Golang's generic interface limitation +type OptionInterface interface { + GetDefaultValue() any +} + +type Option[T any] struct { mu sync.RWMutex cfgSecKey CfgSecKey dynKey string - def, value T + value T + defSimple T + defFunc func() T + emptyAsDef bool + has bool revision int } -func (value *Value[T]) parse(key, valStr string) (v T) { - v = value.def +func (opt *Option[T]) GetDefaultValue() any { + return opt.DefaultValue() +} + +func (opt *Option[T]) parse(key, valStr string) (v T) { + v = opt.DefaultValue() if valStr != "" { if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil { log.Error("Unable to unmarshal json config for key %q, err: %v", key, err) @@ -36,7 +50,35 @@ func (value *Value[T]) parse(key, valStr string) (v T) { return v } -func (value *Value[T]) Value(ctx context.Context) (v T) { +func (opt *Option[T]) HasValue(ctx context.Context) bool { + _, _, has := opt.ValueRevision(ctx) + return has +} + +func (opt *Option[T]) Value(ctx context.Context) (v T) { + v, _, _ = opt.ValueRevision(ctx) + return v +} + +func isZeroOrEmpty(v any) bool { + if v == nil { + return true // interface itself is nil + } + r := reflect.ValueOf(v) + if r.IsZero() { + return true + } + + if r.Kind() == reflect.Slice || r.Kind() == reflect.Map { + if r.IsNil() { + return true + } + return r.Len() == 0 + } + return false +} + +func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) { dg := GetDynGetter() if dg == nil { // this is an edge case: the database is not initialized but the system setting is going to be used @@ -44,55 +86,96 @@ func (value *Value[T]) Value(ctx context.Context) (v T) { panic("no config dyn value getter") } - rev := dg.GetRevision(ctx) + rev = dg.GetRevision(ctx) // if the revision in the database doesn't change, use the last value - value.mu.RLock() - if rev == value.revision { - v = value.value - value.mu.RUnlock() - return v + opt.mu.RLock() + if rev == opt.revision { + v = opt.value + has = opt.has + opt.mu.RUnlock() + return v, rev, has } - value.mu.RUnlock() + opt.mu.RUnlock() // try to parse the config and cache it var valStr *string - if dynVal, has := dg.GetValue(ctx, value.dynKey); has { + if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue { valStr = &dynVal - } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(value.cfgSecKey.Sec, value.cfgSecKey.Key); has { + } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); has { valStr = &cfgVal } if valStr == nil { - v = value.def + v = opt.DefaultValue() + has = false } else { - v = value.parse(value.dynKey, *valStr) + v = opt.parse(opt.dynKey, *valStr) + if opt.emptyAsDef && isZeroOrEmpty(v) { + v = opt.DefaultValue() + } else { + has = true + } } - value.mu.Lock() - value.value = v - value.revision = rev - value.mu.Unlock() + opt.mu.Lock() + opt.value = v + opt.revision = rev + opt.has = has + opt.mu.Unlock() + return v, rev, has +} + +func (opt *Option[T]) DynKey() string { + return opt.dynKey +} + +// WithDefaultFunc sets the default value with a function +// The "def" value might be changed during runtime (e.g.: Unmarshal with default), so it shouldn't use the same pointer or slice +func (opt *Option[T]) WithDefaultFunc(f func() T) *Option[T] { + opt.defFunc = f + return opt +} + +func (opt *Option[T]) WithDefaultSimple(def T) *Option[T] { + v := any(def) + switch v.(type) { + case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + default: + // TODO: use reflect to support convertable basic types like `type State string` + r := reflect.ValueOf(v) + if r.Kind() != reflect.Struct { + panic("invalid type for default value, use WithDefaultFunc instead") + } + } + opt.defSimple = def + return opt +} + +func (opt *Option[T]) WithEmptyAsDefault() *Option[T] { + opt.emptyAsDef = true + return opt +} + +func (opt *Option[T]) DefaultValue() T { + if opt.defFunc != nil { + return opt.defFunc() + } + return opt.defSimple +} + +func (opt *Option[T]) WithFileConfig(cfgSecKey CfgSecKey) *Option[T] { + opt.cfgSecKey = cfgSecKey + return opt +} + +var allConfigOptions = map[string]OptionInterface{} + +func NewOption[T any](dynKey string) *Option[T] { + v := &Option[T]{dynKey: dynKey} + allConfigOptions[dynKey] = v return v } -func (value *Value[T]) DynKey() string { - return value.dynKey -} - -func (value *Value[T]) WithDefault(def T) *Value[T] { - value.def = def - return value -} - -func (value *Value[T]) DefaultValue() T { - return value.def -} - -func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] { - value.cfgSecKey = cfgSecKey - return value -} - -func ValueJSON[T any](dynKey string) *Value[T] { - return &Value[T]{dynKey: dynKey} +func GetConfigOption(dynKey string) OptionInterface { + return allConfigOptions[dynKey] } diff --git a/modules/setting/config_option_instance.go b/modules/setting/config_option_instance.go new file mode 100644 index 0000000000..6d97055a75 --- /dev/null +++ b/modules/setting/config_option_instance.go @@ -0,0 +1,58 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "time" + + "code.gitea.io/gitea/modules/setting/config" +) + +// WebBannerType fields are directly used in templates, +// do remember to update the template if you change the fields +type WebBannerType struct { + DisplayEnabled bool + ContentMessage string + StartTimeUnix int64 + EndTimeUnix int64 +} + +func (b WebBannerType) ShouldDisplay() bool { + if !b.DisplayEnabled || b.ContentMessage == "" { + return false + } + now := time.Now().Unix() + if b.StartTimeUnix > 0 && now < b.StartTimeUnix { + return false + } + if b.EndTimeUnix > 0 && now > b.EndTimeUnix { + return false + } + return true +} + +type MaintenanceModeType struct { + AdminWebAccessOnly bool + StartTimeUnix int64 + EndTimeUnix int64 +} + +func (m MaintenanceModeType) IsActive() bool { + if !m.AdminWebAccessOnly { + return false + } + now := time.Now().Unix() + if m.StartTimeUnix > 0 && now < m.StartTimeUnix { + return false + } + if m.EndTimeUnix > 0 && now > m.EndTimeUnix { + return false + } + return true +} + +type InstanceStruct struct { + WebBanner *config.Option[WebBannerType] + MaintenanceMode *config.Option[MaintenanceModeType] +} diff --git a/modules/web/middleware/cookie.go b/modules/web/middleware/cookie.go index f98aceba10..336c276fe8 100644 --- a/modules/web/middleware/cookie.go +++ b/modules/web/middleware/cookie.go @@ -14,7 +14,11 @@ import ( "code.gitea.io/gitea/modules/util" ) -const cookieRedirectTo = "redirect_to" +const ( + CookieWebBannerDismissed = "gitea_disbnr" + CookieTheme = "gitea_theme" + cookieRedirectTo = "redirect_to" +) func GetRedirectToCookie(req *http.Request) string { return GetSiteCookie(req, cookieRedirectTo) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 97e2ebe0d1..bcd28f2deb 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -84,6 +84,7 @@ "save": "Save", "add": "Add", "add_all": "Add All", + "dismiss": "Dismiss", "remove": "Remove", "remove_all": "Remove All", "remove_label_str": "Remove item \"%s\"", @@ -3278,6 +3279,13 @@ "admin.config.cache_test_failed": "Failed to probe the cache: %v.", "admin.config.cache_test_slow": "Cache test successful, but response is slow: %s.", "admin.config.cache_test_succeeded": "Cache test successful, got a response in %s.", + "admin.config.common.start_time": "Start time", + "admin.config.common.end_time": "End time", + "admin.config.common.skip_time_check": "Leave time empty (clear the field) to skip time check", + "admin.config.instance_maintenance": "Instance Maintenance", + "admin.config.instance_maintenance_mode.admin_web_access_only": "Only allow admin to access the web UI", + "admin.config.instance_web_banner.enabled": "Show banner", + "admin.config.instance_web_banner.message_placeholder": "Banner message (supports markdown)", "admin.config.session_config": "Session Configuration", "admin.config.session_provider": "Session Provider", "admin.config.provider_config": "Provider Config", diff --git a/routers/common/errpage.go b/routers/common/errpage.go index b14ab8bcf8..2406cf443f 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" @@ -36,9 +37,7 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in w.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions) } - tmplCtx := context.NewTemplateContext(req.Context(), req) - tmplCtx["Locale"] = middleware.Locale(w, req) - + tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req)) w.WriteHeader(respCode) outBuf := &bytes.Buffer{} diff --git a/routers/common/maintenancemode.go b/routers/common/maintenancemode.go new file mode 100644 index 0000000000..b5827ac94f --- /dev/null +++ b/routers/common/maintenancemode.go @@ -0,0 +1,43 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +func isMaintenanceModeAllowedRequest(req *http.Request) bool { + if strings.HasPrefix(req.URL.Path, "/-/") { + // URLs like "/-/admin", "/-/fetch-redirect" and "/-/markup" are still accessible in maintenance mode + return true + } + if strings.HasPrefix(req.URL.Path, "/api/internal/") { + // internal APIs should be allowed + return true + } + if strings.HasPrefix(req.URL.Path, "/user/") { + // URLs like "/user/signin" and "/user/signup" are still accessible in maintenance mode + return true + } + if strings.HasPrefix(req.URL.Path, "/assets/") { + return true + } + return false +} + +func MaintenanceModeHandler() func(h http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + maintenanceMode := setting.Config().Instance.MaintenanceMode.Value(req.Context()) + if maintenanceMode.IsActive() && !isMaintenanceModeAllowedRequest(req) { + renderServiceUnavailable(resp, req) + return + } + next.ServeHTTP(resp, req) + }) + } +} diff --git a/routers/init.go b/routers/init.go index 82a5378263..8874236a60 100644 --- a/routers/init.go +++ b/routers/init.go @@ -181,6 +181,7 @@ func InitWebInstalled(ctx context.Context) { func NormalRoutes() *web.Router { r := web.NewRouter() r.Use(common.ProtocolMiddlewares()...) + r.Use(common.MaintenanceModeHandler()) r.Mount("/", web_routers.Routes()) r.Mount("/api/v1", apiv1.Routes()) diff --git a/routers/private/internal.go b/routers/private/internal.go index 55a11aa3dd..2d5436468b 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common" + "code.gitea.io/gitea/routers/web/misc" "code.gitea.io/gitea/services/context" "gitea.com/go-chi/binding" @@ -59,6 +60,7 @@ func Routes() *web.Router { // Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers. r.Use(chi_middleware.RealIP) + r.Get("/dummy", misc.DummyOK) r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 774b31ab98..79e969fd5e 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -5,9 +5,9 @@ package admin import ( + "errors" "net/http" "net/url" - "strconv" "strings" system_model "code.gitea.io/gitea/models/system" @@ -145,7 +145,6 @@ func Config(ctx *context.Context) { ctx.Data["Service"] = setting.Service ctx.Data["DbCfg"] = setting.Database ctx.Data["Webhook"] = setting.Webhook - ctx.Data["MailerEnabled"] = false if setting.MailService != nil { ctx.Data["MailerEnabled"] = true @@ -191,52 +190,27 @@ func ConfigSettings(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.config_settings") ctx.Data["PageIsAdminConfig"] = true ctx.Data["PageIsAdminConfigSettings"] = true - ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString() ctx.HTML(http.StatusOK, tplConfigSettings) } +func validateConfigKeyValue(dynKey, input string) error { + opt := config.GetConfigOption(dynKey) + if opt == nil { + return util.NewInvalidArgumentErrorf("unknown config key: %s", dynKey) + } + + const limit = 64 * 1024 + if len(input) > limit { + return util.NewInvalidArgumentErrorf("value length exceeds limit of %d", limit) + } + + if !json.Valid([]byte(input)) { + return util.NewInvalidArgumentErrorf("invalid json value for key: %s", dynKey) + } + return nil +} + func ChangeConfig(ctx *context.Context) { - cfg := setting.Config() - - marshalBool := func(v string) ([]byte, error) { - b, _ := strconv.ParseBool(v) - return json.Marshal(b) - } - - marshalString := func(emptyDefault string) func(v string) ([]byte, error) { - return func(v string) ([]byte, error) { - return json.Marshal(util.IfZero(v, emptyDefault)) - } - } - - marshalOpenWithApps := func(value string) ([]byte, error) { - // TODO: move the block alongside OpenWithEditorAppsType.ToTextareaString - lines := strings.Split(value, "\n") - var openWithEditorApps setting.OpenWithEditorAppsType - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - displayName, openURL, ok := strings.Cut(line, "=") - displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL) - if !ok || displayName == "" || openURL == "" { - continue - } - openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{ - DisplayName: strings.TrimSpace(displayName), - OpenURL: strings.TrimSpace(openURL), - }) - } - return json.Marshal(openWithEditorApps) - } - marshallers := map[string]func(string) ([]byte, error){ - cfg.Picture.DisableGravatar.DynKey(): marshalBool, - cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool, - cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps, - cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()), - } - _ = ctx.Req.ParseForm() configKeys := ctx.Req.Form["key"] configValues := ctx.Req.Form["value"] @@ -249,18 +223,16 @@ loop: } value := configValues[i] - marshaller, hasMarshaller := marshallers[key] - if !hasMarshaller { - ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) - break loop - } - - marshaledValue, err := marshaller(value) + err := validateConfigKeyValue(key, value) if err != nil { - ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) + if errors.Is(err, util.ErrInvalidArgument) { + ctx.JSONError(err.Error()) + } else { + ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) + } break loop } - configSettings[key] = string(marshaledValue) + configSettings[key] = value } if ctx.Written() { return diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index bc0939d92a..9529525a27 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -162,6 +162,11 @@ func consumeAuthRedirectLink(ctx *context.Context) string { } func redirectAfterAuth(ctx *context.Context) { + if setting.Config().Instance.MaintenanceMode.Value(ctx).IsActive() { + // in maintenance mode, redirect to admin dashboard, it is the only accessible page + ctx.Redirect(setting.AppSubURL + "/-/admin") + return + } ctx.RedirectToCurrentSite(consumeAuthRedirectLink(ctx)) } diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index 59b97c1717..3d2f624263 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -6,12 +6,15 @@ package misc import ( "net/http" "path" + "strconv" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/context" ) func SSHInfo(rw http.ResponseWriter, req *http.Request) { @@ -47,3 +50,9 @@ func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request http.Redirect(w, req, path.Join(setting.StaticURLPrefix, target), http.StatusMovedPermanently) } } + +func WebBannerDismiss(ctx *context.Context) { + _, rev, _ := setting.Config().Instance.WebBanner.ValueRevision(ctx) + middleware.SetSiteCookie(ctx.Resp, middleware.CookieWebBannerDismissed, strconv.Itoa(rev), 48*3600) + ctx.JSONOK() +} diff --git a/routers/web/misc/webtheme.go b/routers/web/misc/webtheme.go index 076bdf8fda..76ddf4b567 100644 --- a/routers/web/misc/webtheme.go +++ b/routers/web/misc/webtheme.go @@ -37,6 +37,6 @@ func WebThemeApply(ctx *context.Context) { opts := &user_service.UpdateOptions{Theme: optional.Some(themeName)} _ = user_service.UpdateUser(ctx, ctx.Doer, opts) } else { - middleware.SetSiteCookie(ctx.Resp, "gitea_theme", themeName, 0) + middleware.SetSiteCookie(ctx.Resp, middleware.CookieTheme, themeName, 0) } } diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 00d30bedef..d1a969cf2d 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -69,9 +69,6 @@ func prepareHomeSidebarRepoTopics(ctx *context.Context) { func prepareOpenWithEditorApps(ctx *context.Context) { var tmplApps []map[string]any apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx) - if len(apps) == 0 { - apps = setting.DefaultOpenWithEditorApps() - } for _, app := range apps { schema, _, _ := strings.Cut(app.OpenURL, ":") diff --git a/routers/web/web.go b/routers/web/web.go index b1b31a7ec9..ce037afe1b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -480,7 +480,7 @@ func registerWebRoutes(m *web.Router) { }, optionsCorsHandler()) m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup) - + m.Post("/-/web-banner/dismiss", misc.WebBannerDismiss) m.Get("/-/web-theme/list", misc.WebThemeList) m.Post("/-/web-theme/apply", optSignIn, misc.WebThemeApply) diff --git a/services/context/context.go b/services/context/context.go index ccd0057f59..97b9890f43 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -100,12 +100,12 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) { return ctx } -func NewTemplateContextForWeb(ctx *Context) TemplateContext { - tmplCtx := NewTemplateContext(ctx, ctx.Req) - tmplCtx["Locale"] = ctx.Base.Locale +func NewTemplateContextForWeb(ctx reqctx.RequestContext, req *http.Request, locale translation.Locale) TemplateContext { + tmplCtx := NewTemplateContext(ctx, req) + tmplCtx["Locale"] = locale tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx) tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx) - tmplCtx["RootData"] = ctx.Data + tmplCtx["RootData"] = ctx.GetData() tmplCtx["Consts"] = map[string]any{ "RepoUnitTypeCode": unit.TypeCode, "RepoUnitTypeIssues": unit.TypeIssues, @@ -132,7 +132,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { Repo: &Repository{}, Org: &Organization{}, } - ctx.TemplateContext = NewTemplateContextForWeb(ctx) + ctx.TemplateContext = NewTemplateContextForWeb(ctx, ctx.Base.Req, ctx.Base.Locale) ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} ctx.SetContextValue(WebContextKey, ctx) return ctx diff --git a/services/context/context_template.go b/services/context/context_template.go index c1045136ee..52c7461187 100644 --- a/services/context/context_template.go +++ b/services/context/context_template.go @@ -6,8 +6,11 @@ package context import ( "context" "net/http" + "strconv" "time" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/webtheme" ) @@ -17,6 +20,10 @@ func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext return TemplateContext{"_ctx": ctx, "_req": req} } +func (c TemplateContext) req() *http.Request { + return c["_req"].(*http.Request) +} + func (c TemplateContext) parentContext() context.Context { return c["_ctx"].(context.Context) } @@ -38,7 +45,6 @@ func (c TemplateContext) Value(key any) any { } func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo { - req := c["_req"].(*http.Request) var themeName string if webCtx := GetWebContext(c); webCtx != nil { if webCtx.Doer != nil { @@ -46,9 +52,20 @@ func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo { } } if themeName == "" { - if cookieTheme, _ := req.Cookie("gitea_theme"); cookieTheme != nil { - themeName = cookieTheme.Value - } + themeName = middleware.GetSiteCookie(c.req(), middleware.CookieTheme) } return webtheme.GuaranteeGetThemeMetaInfo(themeName) } + +func (c TemplateContext) CurrentWebBanner() *setting.WebBannerType { + // Using revision as a simple approach to determine if the banner has been changed after the user dismissed it. + // There could be some false-positives because revision can be changed even if the banner isn't. + // While it should be still good enough (no admin would keep changing the settings) and doesn't really harm end users (just a few more times to see the banner) + // So it doesn't need to make it more complicated by allocating unique IDs or using hashes. + dismissedBannerRevision, _ := strconv.Atoi(middleware.GetSiteCookie(c.req(), middleware.CookieWebBannerDismissed)) + banner, revision, _ := setting.Config().Instance.WebBanner.ValueRevision(c) + if banner.ShouldDisplay() && dismissedBannerRevision != revision { + return &banner + } + return nil +} diff --git a/templates/admin/config_settings/config_settings.tmpl b/templates/admin/config_settings/config_settings.tmpl index 1ef764a58b..6d1db4f89f 100644 --- a/templates/admin/config_settings/config_settings.tmpl +++ b/templates/admin/config_settings/config_settings.tmpl @@ -1,7 +1,7 @@ -{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}} +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config" "dataGlobalInit" "initAdminConfigSettings")}} -{{template "admin/config_settings/avatars" .}} - -{{template "admin/config_settings/repository" .}} + {{template "admin/config_settings/avatars" .}} + {{template "admin/config_settings/repository" .}} + {{template "admin/config_settings/instance" .}} {{template "admin/layout_footer" .}} diff --git a/templates/admin/config_settings/instance.tmpl b/templates/admin/config_settings/instance.tmpl new file mode 100644 index 0000000000..da28fffddb --- /dev/null +++ b/templates/admin/config_settings/instance.tmpl @@ -0,0 +1,63 @@ +

{{ctx.Locale.Tr "admin.config.instance_maintenance"}}

+
+
+ {{$cfgOpt := $.SystemConfig.Instance.MaintenanceMode}} + {{$cfgKey := $cfgOpt.DynKey}} + {{$maintenanceMode := $cfgOpt.Value ctx}} + +
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
{{ctx.Locale.Tr "admin.config.common.skip_time_check"}}
+
+ +
+ + {{$cfgOpt = $.SystemConfig.Instance.WebBanner}} + {{$cfgKey = $cfgOpt.DynKey}} + {{$banner := $cfgOpt.Value ctx}} + +
+
+ + +
+ {{template "shared/combomarkdowneditor" (dict + "ContainerClasses" "web-banner-content-editor" + "TextareaName" (print $cfgKey ".ContentMessage") + "TextareaContent" $banner.ContentMessage + "TextareaPlaceholder" (ctx.Locale.Tr "admin.config.instance_web_banner.message_placeholder") + )}} +
+
+
+
+ + +
+
+ + +
+
+
{{ctx.Locale.Tr "admin.config.common.skip_time_check"}}
+
+
+ +
+
+
diff --git a/templates/admin/config_settings/repository.tmpl b/templates/admin/config_settings/repository.tmpl index 9a37707835..2d5845ba4e 100644 --- a/templates/admin/config_settings/repository.tmpl +++ b/templates/admin/config_settings/repository.tmpl @@ -2,24 +2,23 @@ {{ctx.Locale.Tr "repository"}}
-
+ + {{$cfg := .SystemConfig.Repository.OpenWithEditorApps}}
{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}} -
{{.DefaultOpenWithEditorAppsString}}
+
{{$cfg.DefaultValue.ToTextareaString}}
- {{$cfg := .SystemConfig.Repository.OpenWithEditorApps}} - - + {{/* TODO: OPEN-WITH-EDITOR-APP-JSON: use a simple textarea */}} +
+ {{$cfg = .SystemConfig.Repository.GitGuideRemoteName}}
- {{$cfg = .SystemConfig.Repository.GitGuideRemoteName}} - - +
diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl index 7cc6624d50..397516da5d 100644 --- a/templates/admin/layout_head.tmpl +++ b/templates/admin/layout_head.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .ctxData}} -
+
{{template "admin/navbar" .ctxData}}
diff --git a/templates/base/head_banner.tmpl b/templates/base/head_banner.tmpl new file mode 100644 index 0000000000..d237161622 --- /dev/null +++ b/templates/base/head_banner.tmpl @@ -0,0 +1,11 @@ +{{$banner := ctx.CurrentWebBanner}} +{{if $banner}} +
+
+ {{ctx.RenderUtils.MarkdownToHtml $banner.ContentMessage}} +
+ +
+{{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index cda1f377b4..28fcee023f 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -176,3 +176,4 @@
{{end}} +{{template "base/head_banner"}} diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index 1c48ebbb95..3c0759f9b2 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -4,7 +4,7 @@ * ContainerClasses: additional classes for the container element * MarkdownPreviewInRepo: the repo to preview markdown * MarkdownPreviewContext: preview context (the related url path when rendering) for the preview tab, eg: repo link or user home link -* MarkdownPreviewMode: content mode for the editor, eg: wiki, comment or default +* MarkdownPreviewMode: content mode for the editor, eg: wiki, comment or default, can be disabled by "none" * TextareaName: name attribute for the textarea * TextareaContent: content for the textarea * TextareaMaxLength: maxlength attribute for the textarea @@ -29,10 +29,12 @@ data-preview-url="{{$previewUrl}}" data-preview-context="{{$previewContext}}" > + {{if ne $previewMode "none"}} + {{end}}
@@ -87,9 +89,9 @@
- + x - +
diff --git a/tests/integration/admin_config_test.go b/tests/integration/admin_config_test.go index eec7e75fd9..5f882e8a55 100644 --- a/tests/integration/admin_config_test.go +++ b/tests/integration/admin_config_test.go @@ -7,10 +7,14 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAdminConfig(t *testing.T) { @@ -20,4 +24,46 @@ func TestAdminConfig(t *testing.T) { req := NewRequest(t, "GET", "/-/admin/config") resp := session.MakeRequest(t, req, http.StatusOK) assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) + + t.Run("OpenEditorWithApps", func(t *testing.T) { + cfg := setting.Config().Repository.OpenWithEditorApps + editorApps := cfg.Value(t.Context()) + assert.Len(t, editorApps, 3) + assert.False(t, cfg.HasValue(t.Context())) + + require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[]"})) + config.GetDynGetter().InvalidateCache() + + editorApps = cfg.Value(t.Context()) + assert.Len(t, editorApps, 3) + assert.False(t, cfg.HasValue(t.Context())) + + require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[{}]"})) + config.GetDynGetter().InvalidateCache() + + editorApps = cfg.Value(t.Context()) + assert.Len(t, editorApps, 1) + assert.True(t, cfg.HasValue(t.Context())) + }) + + t.Run("InstanceWebBanner", func(t *testing.T) { + banner, rev1, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context()) + assert.False(t, has) + assert.Equal(t, setting.WebBannerType{}, banner) + + req = NewRequestWithValues(t, "POST", "/-/admin/config", map[string]string{ + "key": "instance.web_banner", + "value": `{"DisplayEnabled":true,"ContentMessage":"test-msg","StartTimeUnix":123,"EndTimeUnix":456}`, + }) + session.MakeRequest(t, req, http.StatusOK) + banner, rev2, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context()) + assert.NotEqual(t, rev1, rev2) + assert.True(t, has) + assert.Equal(t, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "test-msg", + StartTimeUnix: 123, + EndTimeUnix: 456, + }, banner) + }) } diff --git a/tests/integration/config_instance_test.go b/tests/integration/config_instance_test.go new file mode 100644 index 0000000000..c9aa7ab745 --- /dev/null +++ b/tests/integration/config_instance_test.go @@ -0,0 +1,126 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + "time" + + system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/setting/config" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockSystemConfig[T any](t *testing.T, opt *config.Option[T], v T) func() { + jsonBuf, _ := json.Marshal(v) + old := opt.Value(t.Context()) + require.NoError(t, system_model.SetSettings(t.Context(), map[string]string{opt.DynKey(): string(jsonBuf)})) + config.GetDynGetter().InvalidateCache() + return func() { + jsonBuf, _ := json.Marshal(old) + require.NoError(t, system_model.SetSettings(t.Context(), map[string]string{opt.DynKey(): string(jsonBuf)})) + config.GetDynGetter().InvalidateCache() + } +} + +func TestInstance(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("WebBanner", func(t *testing.T) { + t.Run("Visibility", func(t *testing.T) { + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "Planned **upgrade** in progress.", + })() + + t.Run("AnonymousUserSeesBanner", func(t *testing.T) { + resp := MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK) + assert.Contains(t, resp.Body.String(), "Planned upgrade in progress.") + }) + + t.Run("NormalUserSeesBanner", func(t *testing.T) { + sess := loginUser(t, "user2") + resp := sess.MakeRequest(t, NewRequest(t, "GET", "/user/settings"), http.StatusOK) + assert.Contains(t, resp.Body.String(), "Planned upgrade in progress.") + }) + + t.Run("AdminSeesBannerWithoutEditHint", func(t *testing.T) { + sess := loginUser(t, "user1") + resp := sess.MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusOK) + assert.Contains(t, resp.Body.String(), "Planned upgrade in progress.") + assert.NotContains(t, resp.Body.String(), "Edit this banner") + }) + + t.Run("APIRequestUnchanged", func(t *testing.T) { + MakeRequest(t, NewRequest(t, "GET", "/api/v1/version"), http.StatusOK) + }) + }) + + t.Run("TimeWindow", func(t *testing.T) { + now := time.Now().Unix() + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "Future banner", + StartTimeUnix: now + 3600, + EndTimeUnix: now + 7200, + })() + + resp := MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK) + assert.NotContains(t, resp.Body.String(), "Future banner") + + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "Expired banner", + StartTimeUnix: now - 7200, + EndTimeUnix: now - 3600, + })() + + resp = MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK) + assert.NotContains(t, resp.Body.String(), "Expired banner") + }) + }) + + t.Run("MaintenanceMode", func(t *testing.T) { + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "MaintenanceModeBanner", + })() + defer mockSystemConfig(t, setting.Config().Instance.MaintenanceMode, setting.MaintenanceModeType{AdminWebAccessOnly: true})() + + t.Run("AnonymousUser", func(t *testing.T) { + req := NewRequest(t, "GET", "/") + req.Header.Add("Accept", "text/html") + resp := MakeRequest(t, req, http.StatusServiceUnavailable) + assert.Contains(t, resp.Body.String(), "MaintenanceModeBanner") + assert.Contains(t, resp.Body.String(), `href="/user/login"`) // it must contain the login link + + MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusOK) + MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusSeeOther) + MakeRequest(t, NewRequest(t, "GET", "/api/internal/dummy"), http.StatusForbidden) + }) + + t.Run("AdminLogin", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{"user_name": "user1", "password": userPassword}) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/-/admin", resp.Header().Get("Location")) + + sess := loginUser(t, "user1") + req = NewRequest(t, "GET", "/") + req.Header.Add("Accept", "text/html") + resp = sess.MakeRequest(t, req, http.StatusServiceUnavailable) + assert.Contains(t, resp.Body.String(), "MaintenanceModeBanner") + + resp = sess.MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusSeeOther) + assert.Equal(t, "/-/admin", resp.Header().Get("Location")) + + sess.MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusOK) + }) + }) +} diff --git a/web_src/css/admin.css b/web_src/css/admin.css index cda38c6ddd..d84aa7e811 100644 --- a/web_src/css/admin.css +++ b/web_src/css/admin.css @@ -49,3 +49,11 @@ gap: 1rem; margin-bottom: 1rem; } + +.web-banner-content-editor .render-content.render-preview { + /* use the styles from ".ui.message" */ + padding: 1em 1.5em; + border: 1px solid var(--color-info-border); + background: var(--color-info-bg); + color: var(--color-info-text); +} diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css index 236cb986fd..1b2a1d64b7 100644 --- a/web_src/css/modules/container.css +++ b/web_src/css/modules/container.css @@ -14,3 +14,21 @@ .ui.container.medium-width { width: 800px; } + +.ui.message.web-banner-container { + position: relative; + margin: 0; + border-radius: 0; +} + +.ui.message.web-banner-container > .web-banner-content { + width: 1280px; + max-width: calc(100% - calc(2 * var(--page-margin-x))); + margin: auto; +} + +.ui.message.web-banner-container > button.dismiss-banner { + position: absolute; + right: 20px; + top: 15px; +} diff --git a/web_src/js/features/admin/config.test.ts b/web_src/js/features/admin/config.test.ts new file mode 100644 index 0000000000..e44ccb2a94 --- /dev/null +++ b/web_src/js/features/admin/config.test.ts @@ -0,0 +1,41 @@ +import {ConfigFormValueMapper} from './config.ts'; + +test('ConfigFormValueMapper', () => { + document.body.innerHTML = ` + + + + + + + + + + + + + + + +`; + + const form = document.querySelector('form')!; + const mapper = new ConfigFormValueMapper(form); + mapper.fillFromSystemConfig(); + const formData = mapper.collectToFormData(); + const result: Record = {}; + const keys = [], values = []; + for (const [key, value] of formData.entries()) { + if (key === 'key') keys.push(value as string); + if (key === 'value') values.push(value as string); + } + for (let i = 0; i < keys.length; i++) { + result[keys[i]] = values[i]; + } + expect(result).toEqual({ + 'k1': 'true', + 'k2': '"k2-val"', + 'repository.open-with.editor-apps': '[{"DisplayName":"a","OpenURL":"b"}]', // TODO: OPEN-WITH-EDITOR-APP-JSON: it must match backend + 'struct': '{"SubBoolean":true,"SubTimestamp":123456780,"OtherKey":"other-value","NewKey":"new-value"}', + }); +}); diff --git a/web_src/js/features/admin/config.ts b/web_src/js/features/admin/config.ts index 76f7c1db50..047c1a46a4 100644 --- a/web_src/js/features/admin/config.ts +++ b/web_src/js/features/admin/config.ts @@ -1,24 +1,210 @@ import {showTemporaryTooltip} from '../../modules/tippy.ts'; import {POST} from '../../modules/fetch.ts'; +import {registerGlobalInitFunc} from '../../modules/observer.ts'; +import {queryElems} from '../../utils/dom.ts'; +import {submitFormFetchAction} from '../common-fetch-action.ts'; const {appSubUrl} = window.config; -export function initAdminConfigs(): void { - const elAdminConfig = document.querySelector('.page-content.admin.config'); - if (!elAdminConfig) return; +function initSystemConfigAutoCheckbox(el: HTMLInputElement) { + el.addEventListener('change', async () => { + // if the checkbox is inside a form, we assume it's handled by the form submit and do not send an individual request + if (el.closest('form')) return; + try { + const resp = await POST(`${appSubUrl}/-/admin/config`, { + data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key')!, value: String(el.checked)}), + }); + const json: Record = await resp.json(); + if (json.errorMessage) throw new Error(json.errorMessage); + } catch (ex) { + showTemporaryTooltip(el, ex.toString()); + el.checked = !el.checked; + } + }); +} - for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) { - el.addEventListener('change', async () => { +type GeneralFormFieldElement = HTMLInputElement; + +function unsupportedElement(el: Element): never { + // HINT: for future developers: if you need to handle a config that cannot be directly mapped to a form element, you should either: + // * Add a "hidden" input to store the value (not configurable) + // * Design a new "component" to handle the config + throw new Error(`Unsupported config form value mapping for ${el.nodeName} (name=${(el as HTMLInputElement).name},type=${(el as HTMLInputElement).type}), please add more and design carefully`); +} + +function requireExplicitValueType(el: Element): never { + throw new Error(`Unsupported config form value type for ${el.nodeName} (name=${(el as HTMLInputElement).name},type=${(el as HTMLInputElement).type}), please add explicit value type with "data-config-value-type" attribute`); +} + +// try to extract the subKey for the config value from the element name +// * return '' if the element name exactly matches the config key, which means the value is directly stored in the element +// * return null if the config key not match +function extractElemConfigSubKey(el: GeneralFormFieldElement, dynKey: string): string | null { + if (el.name === dynKey) return ''; + if (el.name.startsWith(`${dynKey}.`)) return el.name.slice(dynKey.length + 1); // +1 for the dot + return null; +} + +// Due to the different design between HTML form elements and the JSON struct of the config values, we need to explicitly define some types. +// * checkbox can be used for boolean value, it can also be used for multiple values (array) +type ConfigValueType = 'boolean' | 'string' | 'number' | 'timestamp'; // TODO: support more types like array, not used at the moment. + +function toDatetimeLocalValue(unixSeconds: number) { + const d = new Date(unixSeconds * 1000); + return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16); +} + +export class ConfigFormValueMapper { + form: HTMLFormElement; + presetJsonValues: Record = {}; + presetValueTypes: Record = {}; + + constructor(form: HTMLFormElement) { + this.form = form; + for (const el of queryElems(form, '[data-config-value-json]')) { + const dynKey = el.getAttribute('data-config-dyn-key')!; + const jsonStr = el.getAttribute('data-config-value-json'); try { - const resp = await POST(`${appSubUrl}/-/admin/config`, { - data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key')!, value: String(el.checked)}), - }); - const json: Record = await resp.json(); - if (json.errorMessage) throw new Error(json.errorMessage); - } catch (ex) { - showTemporaryTooltip(el, ex.toString()); - el.checked = !el.checked; + this.presetJsonValues[dynKey] = JSON.parse(jsonStr || '{}'); // empty string also is valid, default to an empty object + } catch (error) { + this.presetJsonValues[dynKey] = {}; // in case the value in database is corrupted, don't break the whole form + console.error(`Error parsing JSON for config ${dynKey}:`, error); } - }); + } + for (const el of queryElems(form, '[data-config-value-type]')) { + const valKey = el.getAttribute('data-config-dyn-key') || el.name; + this.presetValueTypes[valKey] = el.getAttribute('data-config-value-type')! as ConfigValueType; + } + } + + // try to assign the config value to the form element, return true if assigned successfully, + // otherwise return false (e.g. the element is not related to the config key) + assignConfigValueToFormElement(el: GeneralFormFieldElement, dynKey: string, cfgVal: any) { + const subKey = extractElemConfigSubKey(el, dynKey); + if (subKey === null) return false; // if not match, skip + + const val = subKey ? cfgVal![subKey] : cfgVal; + if (val === null) return true; // if name matches, but no value to assign, also succeed because the form element does exist + const valType = this.presetValueTypes[el.name]; + if (el.matches('[type="checkbox"]')) { + if (valType !== 'boolean') requireExplicitValueType(el); + el.checked = Boolean(val ?? el.checked); + } else if (el.matches('[type="datetime-local"]')) { + if (valType !== 'timestamp') requireExplicitValueType(el); + if (val) el.value = toDatetimeLocalValue(val); + } else if (el.matches('textarea')) { + el.value = String(val ?? el.value); + } else if (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text') { + el.value = String(val ?? el.value); + } else { + unsupportedElement(el); + } + return true; + } + + collectConfigValueFromElement(el: GeneralFormFieldElement, _oldVal: any = null) { + let val: any; + const valType = this.presetValueTypes[el.name]; + if (el.matches('[type="checkbox"]')) { + if (valType !== 'boolean') requireExplicitValueType(el); + val = el.checked; + // oldVal: for future use when we support array value with checkbox + } else if (el.matches('[type="datetime-local"]')) { + if (valType !== 'timestamp') requireExplicitValueType(el); + val = Math.floor(new Date(el.value).getTime() / 1000) ?? 0; // NaN is fine to JSON.stringify, it becomes null. + } else if (el.matches('textarea')) { + val = el.value; + } else if (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text') { + val = el.value; + } else { + unsupportedElement(el); + } + return val; + } + + collectConfigSubValues(namedElems: Array, dynKey: string, cfgVal: Record) { + for (let idx = 0; idx < namedElems.length; idx++) { + const el = namedElems[idx]; + if (!el) continue; + const subKey = extractElemConfigSubKey(el, dynKey); + if (!subKey) continue; // if not match, skip + cfgVal[subKey] = this.collectConfigValueFromElement(el, cfgVal[subKey]); + namedElems[idx] = null; + } + } + + fillFromSystemConfig() { + for (const [dynKey, cfgVal] of Object.entries(this.presetJsonValues)) { + const elems = this.form.querySelectorAll(`[name^="${CSS.escape(dynKey)}"]`); + let assigned = false; + for (const el of elems) { + if (this.assignConfigValueToFormElement(el, dynKey, cfgVal)) { + assigned = true; + } + } + if (!assigned) throw new Error(`Could not find form element for config ${dynKey}, please check the form design and json struct`); + } + } + + // TODO: OPEN-WITH-EDITOR-APP-JSON: need to use the same logic as backend + marshalConfigValueOpenWithEditorApps(cfgVal: string): string { + const apps: Array<{DisplayName: string, OpenURL: string}> = []; + const lines = cfgVal.split('\n'); + for (const line of lines) { + let [displayName, openUrl] = line.split('=', 2); + displayName = displayName.trim(); + openUrl = openUrl?.trim() ?? ''; + if (!displayName || !openUrl) continue; + apps.push({DisplayName: displayName, OpenURL: openUrl}); + } + return JSON.stringify(apps); + } + + marshalConfigValue(dynKey: string, cfgVal: any): string { + if (dynKey === 'repository.open-with.editor-apps') return this.marshalConfigValueOpenWithEditorApps(cfgVal); + return JSON.stringify(cfgVal); + } + + collectToFormData(): FormData { + const namedElems: Array = []; + queryElems(this.form, '[name]', (el) => namedElems.push(el as GeneralFormFieldElement)); + + // first, process the config options with sub values, for example: + // merge "foo.bar.Enabled", "foo.bar.Message" to "foo.bar" + const formData = new FormData(); + for (const [dynKey, cfgVal] of Object.entries(this.presetJsonValues)) { + this.collectConfigSubValues(namedElems, dynKey, cfgVal); + formData.append('key', dynKey); + formData.append('value', this.marshalConfigValue(dynKey, cfgVal)); + } + + // now, the namedElems should only contain the config options without sub values, + // directly store the value in formData with key as the element name, for example: + for (const el of namedElems) { + if (!el) continue; + const dynKey = el.name; + const newVal = this.collectConfigValueFromElement(el); + formData.append('key', dynKey); + formData.append('value', this.marshalConfigValue(dynKey, newVal)); + } + return formData; } } + +function initSystemConfigForm(form: HTMLFormElement) { + const formMapper = new ConfigFormValueMapper(form); + formMapper.fillFromSystemConfig(); + form.addEventListener('submit', async (e) => { + if (!form.reportValidity()) return; + e.preventDefault(); + const formData = formMapper.collectToFormData(); + await submitFormFetchAction(form, {formData}); + }); +} + +export function initAdminConfigs(): void { + registerGlobalInitFunc('initAdminConfigSettings', (el) => { + queryElems(el, 'input[type="checkbox"][data-config-dyn-key]', initSystemConfigAutoCheckbox); + queryElems(el, 'form.system-config-form', initSystemConfigForm); + }); +} diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 0714de95c8..0d72fb32c9 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -67,10 +67,15 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R async function onFormFetchActionSubmit(formEl: HTMLFormElement, e: SubmitEvent) { e.preventDefault(); - await submitFormFetchAction(formEl, submitEventSubmitter(e)); + await submitFormFetchAction(formEl, {formSubmitter: submitEventSubmitter(e)}); } -export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitter?: HTMLElement) { +type SubmitFormFetchActionOpts = { + formSubmitter?: HTMLElement; + formData?: FormData; +}; + +export async function submitFormFetchAction(formEl: HTMLFormElement, opts: SubmitFormFetchActionOpts = {}) { if (formEl.classList.contains('is-loading')) return; formEl.classList.add('is-loading'); @@ -80,8 +85,8 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt const formMethod = formEl.getAttribute('method') || 'get'; const formActionUrl = formEl.getAttribute('action') || window.location.href; - const formData = new FormData(formEl); - const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')]; + const formData = opts.formData ?? new FormData(formEl); + const [submitterName, submitterValue] = [opts.formSubmitter?.getAttribute('name'), opts.formSubmitter?.getAttribute('value')]; if (submitterName) { formData.append(submitterName, submitterValue || ''); } diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 42104947df..5b470ea03d 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -266,8 +266,8 @@ export class ComboMarkdownEditor { addTableButton.addEventListener('click', () => addTablePanelTippy.show()); addTablePanel.querySelector('.ui.button.primary')!.addEventListener('click', () => { - let rows = parseInt(addTablePanel.querySelector('[name=rows]')!.value); - let cols = parseInt(addTablePanel.querySelector('[name=cols]')!.value); + let rows = parseInt(addTablePanel.querySelector('.add-table-rows')!.value); + let cols = parseInt(addTablePanel.querySelector('.add-table-cols')!.value); rows = Math.max(1, Math.min(100, rows)); cols = Math.max(1, Math.min(100, cols)); replaceTextareaSelection(this.textarea, `\n${this.generateMarkdownTable(rows, cols)}\n\n`); diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index b100cd7c91..4957e83d00 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -197,5 +197,5 @@ export function initRepoEditor() { export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) { // the content is from the server, so it is safe to use innerHTML - previewPanel.innerHTML = html`
${htmlRaw(htmlContent)}
`; + previewPanel.innerHTML = html`
${htmlRaw(htmlContent)}
`; } From f7f55a356f3910689a9fccc0f3e244c2e08dd196 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Feb 2026 20:13:19 +0100 Subject: [PATCH 014/207] Update tool dependencies and fix new lint issues (#36702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Update golangci-lint v2.9.0 → v2.10.1, misspell v0.7.0 → v0.8.0, actionlint v1.7.10 → v1.7.11 - Fix 20 new QF1012 staticcheck findings by using `fmt.Fprintf` instead of `WriteString(fmt.Sprintf(...))` - Fix SA1019: replace deprecated `ecdsa.PublicKey` field access with `PublicKey.Bytes()` for JWK encoding, with SEC 1 validation and curve derived from signing algorithm - Add unit test for `ToJWK()` covering P-256, P-384, and P-521 curves, also verifying correct coordinate padding per RFC 7518 - Remove dead staticcheck linter exclusion for "argument x is overwritten before first use" ## Test plan - [x] `make lint-go` passes with 0 issues - [x] `go test ./services/oauth2_provider/ -run TestECDSASigningKeyToJWK` passes for all curves 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 --- .golangci.yml | 3 - Makefile | 6 +- models/repo/repo.go | 2 +- modules/git/foreachref/format.go | 2 +- routers/web/repo/setting/lfs.go | 4 +- services/gitdiff/gitdiff.go | 8 +-- services/oauth2_provider/jwtsigningkey.go | 11 +++- .../oauth2_provider/jwtsigningkey_test.go | 61 +++++++++++++++++++ services/release/notes.go | 8 +-- services/webhook/discord.go | 2 +- services/webhook/feishu.go | 2 +- services/webhook/matrix.go | 4 +- services/webhook/msteams.go | 4 +- services/webhook/slack.go | 2 +- services/webhook/telegram.go | 2 +- services/webhook/wechatwork.go | 4 +- 16 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 services/oauth2_provider/jwtsigningkey_test.go diff --git a/.golangci.yml b/.golangci.yml index 2b85c89fdc..4e01169dc6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -141,9 +141,6 @@ linters: - linters: - unused text: (?i)swagger - - linters: - - staticcheck - text: (?i)argument x is overwritten before first use - linters: - gocritic text: '(?i)commentFormatting: put a space between `//` and comment text' diff --git a/Makefile b/Makefile index cb7742c5c7..d8fce11ee2 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,13 @@ XGO_VERSION := go-1.25.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 -ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10 +ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest diff --git a/models/repo/repo.go b/models/repo/repo.go index 07b9bf30cc..7b7f5adb41 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -281,7 +281,7 @@ func (repo *Repository) SizeDetailsString() string { var str strings.Builder sizeDetails := repo.SizeDetails() for _, detail := range sizeDetails { - str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) + fmt.Fprintf(&str, "%s: %s, ", detail.Name, base.FileSize(detail.Size)) } return strings.TrimSuffix(str.String(), ", ") } diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index d2f9998fe8..cee21c5b66 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -53,7 +53,7 @@ func (f Format) Flag() string { var formatFlag strings.Builder for i, field := range f.fieldNames { // field key and field value - formatFlag.WriteString(fmt.Sprintf("%s %%(%s)", field, field)) + fmt.Fprintf(&formatFlag, "%s %%(%s)", field, field) if i < len(f.fieldNames)-1 { // note: escape delimiters to allow control characters as diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 8a8015035f..a3a60963d4 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -301,13 +301,13 @@ func LFSFileGet(ctx *context.Context) { if index != len(lines)-1 { line += "\n" } - output.WriteString(fmt.Sprintf(`
  • %s
  • `, index+1, index+1, line)) + fmt.Fprintf(&output, `
  • %s
  • `, index+1, index+1, line) } ctx.Data["FileContent"] = gotemplate.HTML(output.String()) output.Reset() for i := 0; i < len(lines); i++ { - output.WriteString(fmt.Sprintf(`%d`, i+1, i+1)) + fmt.Fprintf(&output, `%d`, i+1, i+1) } ctx.Data["LineNums"] = gotemplate.HTML(output.String()) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 7777cf4a1c..b23e5b1b1c 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1594,10 +1594,10 @@ func generatePatchForUnchangedLineFromReader(reader io.Reader, treePath string, // Generate synthetic patch var patchBuilder strings.Builder - patchBuilder.WriteString(fmt.Sprintf("diff --git a/%s b/%s\n", treePath, treePath)) - patchBuilder.WriteString(fmt.Sprintf("--- a/%s\n", treePath)) - patchBuilder.WriteString(fmt.Sprintf("+++ b/%s\n", treePath)) - patchBuilder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", startLine, len(lines), startLine, len(lines))) + fmt.Fprintf(&patchBuilder, "diff --git a/%s b/%s\n", treePath, treePath) + fmt.Fprintf(&patchBuilder, "--- a/%s\n", treePath) + fmt.Fprintf(&patchBuilder, "+++ b/%s\n", treePath) + fmt.Fprintf(&patchBuilder, "@@ -%d,%d +%d,%d @@\n", startLine, len(lines), startLine, len(lines)) for _, lineContent := range lines { patchBuilder.WriteString(" ") diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 03c7403f75..4898d54166 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -214,13 +214,20 @@ func (key ecdsaSingingKey) VerifyKey() any { func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { pubKey := key.key.Public().(*ecdsa.PublicKey) + // PublicKey.Bytes returns the uncompressed SEC 1 format: 0x04 || X || Y + pubKeyBytes, err := pubKey.Bytes() + if err != nil { + return nil, err + } + + coordLen := (len(pubKeyBytes) - 1) / 2 return map[string]string{ "kty": "EC", "alg": key.SigningMethod().Alg(), "kid": key.id, "crv": pubKey.Params().Name, - "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), - "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), + "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+coordLen]), + "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+coordLen:]), }, nil } diff --git a/services/oauth2_provider/jwtsigningkey_test.go b/services/oauth2_provider/jwtsigningkey_test.go new file mode 100644 index 0000000000..55de81dfd8 --- /dev/null +++ b/services/oauth2_provider/jwtsigningkey_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2_provider + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "math/big" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestECDSASigningKeyToJWK(t *testing.T) { + for _, tc := range []struct { + curve elliptic.Curve + signingMethod jwt.SigningMethod + expectedAlg string + expectedCrv string + coordLen int + }{ + {elliptic.P256(), jwt.SigningMethodES256, "ES256", "P-256", 32}, + {elliptic.P384(), jwt.SigningMethodES384, "ES384", "P-384", 48}, + {elliptic.P521(), jwt.SigningMethodES512, "ES512", "P-521", 66}, + } { + t.Run(tc.expectedCrv, func(t *testing.T) { + privKey, err := ecdsa.GenerateKey(tc.curve, rand.Reader) + require.NoError(t, err) + + signingKey, err := newECDSASingingKey(tc.signingMethod, privKey) + require.NoError(t, err) + + jwk, err := signingKey.ToJWK() + require.NoError(t, err) + + assert.Equal(t, "EC", jwk["kty"]) + assert.Equal(t, tc.expectedAlg, jwk["alg"]) + assert.Equal(t, tc.expectedCrv, jwk["crv"]) + assert.NotEmpty(t, jwk["kid"]) + + // Verify coordinates are the correct fixed length per RFC 7518 / SEC 1 + xBytes, err := base64.RawURLEncoding.DecodeString(jwk["x"]) + require.NoError(t, err) + assert.Len(t, xBytes, tc.coordLen) + + yBytes, err := base64.RawURLEncoding.DecodeString(jwk["y"]) + require.NoError(t, err) + assert.Len(t, yBytes, tc.coordLen) + + // Verify the decoded coordinates reconstruct the original public key point + pubKey := privKey.Public().(*ecdsa.PublicKey) + assert.Equal(t, 0, new(big.Int).SetBytes(xBytes).Cmp(pubKey.X)) + assert.Equal(t, 0, new(big.Int).SetBytes(yBytes).Cmp(pubKey.Y)) + }) + } +} diff --git a/services/release/notes.go b/services/release/notes.go index c9dc75af70..92ee22e604 100644 --- a/services/release/notes.go +++ b/services/release/notes.go @@ -113,7 +113,7 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, for _, pr := range prs { prURL := pr.Issue.HTMLURL(ctx) - builder.WriteString(fmt.Sprintf("* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL)) + fmt.Fprintf(&builder, "* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL) } builder.WriteString("\n") @@ -121,7 +121,7 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, if len(contributors) > 0 { builder.WriteString("## Contributors\n") for _, contributor := range contributors { - builder.WriteString(fmt.Sprintf("* @%s\n", contributor.Name)) + fmt.Fprintf(&builder, "* @%s\n", contributor.Name) } builder.WriteString("\n") } @@ -130,14 +130,14 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, builder.WriteString("## New Contributors\n") for _, contributor := range newContributors { prURL := contributor.Issue.HTMLURL(ctx) - builder.WriteString(fmt.Sprintf("* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL)) + fmt.Fprintf(&builder, "* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL) } builder.WriteString("\n") } builder.WriteString("**Full Changelog**: ") compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) - builder.WriteString(fmt.Sprintf("[%s...%s](%s)", baseRef, tagName, compareURL)) + fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL) builder.WriteByte('\n') return builder.String() } diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 19af779120..c0af7c0243 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -169,7 +169,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) { if utf8.RuneCountInString(message) > 50 { message = fmt.Sprintf("%.47s...", message) } - text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name)) + fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("\n") diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index ecce9acc43..ac581df85a 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -77,7 +77,7 @@ func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) { ) var text strings.Builder - text.WriteString(fmt.Sprintf("[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc)) + fmt.Fprintf(&text, "[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc) // for each commit, generate attachment text for i, commit := range p.Commits { var authorName string diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 63fbbf40a9..fa01ecd0b1 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -174,11 +174,11 @@ func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) { repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) var text strings.Builder - text.WriteString(fmt.Sprintf("[%s] %s pushed %s to %s:
    ", repoLink, p.Pusher.UserName, commitDesc, branchLink)) + fmt.Fprintf(&text, "[%s] %s pushed %s to %s:
    ", repoLink, p.Pusher.UserName, commitDesc, branchLink) // for each commit, generate a new line text for i, commit := range p.Commits { - text.WriteString(fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)) + fmt.Fprintf(&text, "%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("
    ") diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index fa39e7228e..34db903712 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -134,8 +134,8 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) { var text strings.Builder // for each commit, generate attachment text for i, commit := range p.Commits { - text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, - strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)) + fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL, + strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("\n\n") diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 0b3dda467c..94d41d2179 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -211,7 +211,7 @@ func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { var attachmentText strings.Builder // for each commit, generate attachment text for i, commit := range p.Commits { - attachmentText.WriteString(fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name))) + fmt.Fprintf(&attachmentText, "%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)) // add linebreak to each commit but the last if i < len(p.Commits)-1 { attachmentText.WriteString("\n") diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 2abc743fab..8e9a53a5de 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -96,7 +96,7 @@ func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) { var htmlCommits strings.Builder for _, commit := range p.Commits { - htmlCommits.WriteString(fmt.Sprintf("\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n")))) + fmt.Fprintf(&htmlCommits, "\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n"))) if commit.Author != nil { htmlCommits.WriteString(" - " + html.EscapeString(commit.Author.Name)) } diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index da9c6b584c..cac6a700c0 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -86,8 +86,8 @@ func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error } message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n") - text.WriteString(fmt.Sprintf(" > [%s](%s) \r\n >%s \n >%s", commit.ID[:7], commit.URL, - message, authorName)) + fmt.Fprintf(&text, " > [%s](%s) \r\n >%s \n >%s", commit.ID[:7], commit.URL, + message, authorName) // add linebreak to each commit but the last if i < len(p.Commits)-1 { From f9a2a8ae8df81fa5bf23ede004cc51a36d01c53a Mon Sep 17 00:00:00 2001 From: WinterCabbage <54889338+WinterCabbage@users.noreply.github.com> Date: Fri, 27 Feb 2026 03:58:10 +0800 Subject: [PATCH 015/207] Fix milestone/project text overflow in issue sidebar (#36741) Fixes #36732 Co-authored-by: Giteabot --- templates/repo/issue/sidebar/milestone_list.tmpl | 8 ++++---- templates/repo/issue/sidebar/project_list.tmpl | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl index 5bc961ed3c..1442963c93 100644 --- a/templates/repo/issue/sidebar/milestone_list.tmpl +++ b/templates/repo/issue/sidebar/milestone_list.tmpl @@ -18,14 +18,14 @@ {{svg "octicon-search"}}
    - {{end}} - + >
    T
    {{if .RefFullName.IsBranch}} {{$addFilePath := .TreePath}} diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts index 5e26bcc685..95004096d8 100644 --- a/web_src/js/features/heatmap.ts +++ b/web_src/js/features/heatmap.ts @@ -1,5 +1,4 @@ import {createApp} from 'vue'; -import ActivityHeatmap from '../components/ActivityHeatmap.vue'; import {translateMonth, translateDay} from '../utils.ts'; import {GET} from '../modules/fetch.ts'; @@ -46,6 +45,7 @@ export async function initHeatmap() { noDataText: el.getAttribute('data-locale-no-contributions'), }; + const {default: ActivityHeatmap} = await import(/* webpackChunkName: "ActivityHeatmap" */ '../components/ActivityHeatmap.vue'); const View = createApp(ActivityHeatmap, {values, locale}); View.mount(el); el.classList.remove('is-loading'); diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 7a35a3c7ff..8d306b2bab 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -1,5 +1,4 @@ import {createApp} from 'vue'; -import RepoFileSearch from '../components/RepoFileSearch.vue'; import {registerGlobalInitFunc} from '../modules/observer.ts'; const threshold = 50; @@ -69,7 +68,8 @@ export function filterRepoFilesWeighted(files: Array, filter: string) { } export function initRepoFileSearch() { - registerGlobalInitFunc('initRepoFileSearch', (el) => { + registerGlobalInitFunc('initRepoFileSearch', async (el) => { + const {default: RepoFileSearch} = await import(/* webpackChunkName: "RepoFileSearch" */ '../components/RepoFileSearch.vue'); createApp(RepoFileSearch, { repoLink: el.getAttribute('data-repo-link'), currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'), diff --git a/web_src/js/features/repo-issue-pull.ts b/web_src/js/features/repo-issue-pull.ts index 91f27305bd..093f484b42 100644 --- a/web_src/js/features/repo-issue-pull.ts +++ b/web_src/js/features/repo-issue-pull.ts @@ -1,5 +1,4 @@ import {createApp} from 'vue'; -import PullRequestMergeForm from '../components/PullRequestMergeForm.vue'; import {GET, POST} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {createElementFromHTML} from '../utils/dom.ts'; @@ -63,10 +62,11 @@ function initRepoPullRequestCommitStatus(el: HTMLElement) { } } -function initRepoPullRequestMergeForm(box: HTMLElement) { +async function initRepoPullRequestMergeForm(box: HTMLElement) { const el = box.querySelector('#pull-request-merge-form'); if (!el) return; + const {default: PullRequestMergeForm} = await import(/* webpackChunkName: "PullRequestMergeForm" */ '../components/PullRequestMergeForm.vue'); const view = createApp(PullRequestMergeForm); view.mount(el); } diff --git a/web_src/js/markup/refissue.ts b/web_src/js/markup/refissue.ts index ff6fdd624f..f2fcd24f39 100644 --- a/web_src/js/markup/refissue.ts +++ b/web_src/js/markup/refissue.ts @@ -1,7 +1,6 @@ import {queryElems} from '../utils/dom.ts'; import {parseIssueHref} from '../utils.ts'; import {createApp} from 'vue'; -import ContextPopup from '../components/ContextPopup.vue'; import {createTippy, getAttachedTippyInstance} from '../modules/tippy.ts'; export function initMarkupRefIssue(el: HTMLElement) { @@ -20,6 +19,14 @@ function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { if (!issuePathInfo.ownerName) return; const el = document.createElement('div'); + const onShowAsync = async () => { + const {default: ContextPopup} = await import(/* webpackChunkName: "ContextPopup" */ '../components/ContextPopup.vue'); + const view = createApp(ContextPopup, { + // backend: GetIssueInfo + loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, + }); + view.mount(el); + }; const tippy = createTippy(refIssue, { theme: 'default', content: el, @@ -29,13 +36,7 @@ function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { role: 'dialog', interactiveBorder: 5, // onHide() { return false }, // help to keep the popup and debug the layout - onShow: () => { - const view = createApp(ContextPopup, { - // backend: GetIssueInfo - loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, - }); - view.mount(el); - }, + onShow: () => { onShowAsync() }, }); tippy.show(); } From 619db646f54dc1844cad94c1ac801dcd66f7a746 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 27 Feb 2026 20:38:44 +0800 Subject: [PATCH 019/207] Deprecate RenderWithErr (#36769) --- routers/install/install.go | 68 ++++++++++----------- routers/web/admin/auths.go | 16 ++--- routers/web/admin/users.go | 42 ++++++------- routers/web/auth/2fa.go | 4 +- routers/web/auth/auth.go | 28 ++++----- routers/web/auth/linkaccount.go | 10 +-- routers/web/auth/openid.go | 24 ++++---- routers/web/auth/password.go | 24 ++++---- routers/web/org/org.go | 8 +-- routers/web/org/setting.go | 2 +- routers/web/org/teams.go | 8 +-- routers/web/repo/migrate.go | 40 ++++++------ routers/web/repo/milestone.go | 4 +- routers/web/repo/release.go | 14 ++--- routers/web/repo/repo.go | 20 +++--- routers/web/repo/setting/deploy_key.go | 8 +-- routers/web/repo/setting/setting.go | 58 +++++++++--------- routers/web/repo/wiki.go | 6 +- routers/web/user/setting/account.go | 12 ++-- routers/web/user/setting/keys.go | 16 ++--- routers/web/user/setting/security/openid.go | 10 +-- services/context/captcha.go | 2 +- services/context/context_response.go | 8 ++- 23 files changed, 218 insertions(+), 214 deletions(-) diff --git a/routers/install/install.go b/routers/install/install.go index 399128b6ed..1a60fee339 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -141,7 +141,7 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { if (setting.Database.Type == "sqlite3") && len(setting.Database.Path) == 0 { ctx.Data["Err_DbPath"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_db_path"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form) return false } @@ -152,10 +152,10 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { if err = db.InitEngine(ctx); err != nil { if strings.Contains(err.Error(), `Unknown database type: sqlite3`) { ctx.Data["Err_DbType"] = true - ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.com/installation/install-from-binary"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.com/installation/install-from-binary"), tplInstall, form) } else { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) } return false } @@ -163,20 +163,20 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { err = db_install.CheckDatabaseConnection(ctx) if err != nil { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) return false } hasPostInstallationUser, err := db_install.HasPostInstallationUsers(ctx) if err != nil { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_table", "user", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "user", err), tplInstall, form) return false } dbMigrationVersion, err := db_install.GetMigrationVersion(ctx) if err != nil { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_table", "version", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "version", err), tplInstall, form) return false } @@ -185,7 +185,7 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { confirmed := form.ReinstallConfirmFirst && form.ReinstallConfirmSecond && form.ReinstallConfirmThird if !confirmed { ctx.Data["Err_DbInstalledBefore"] = true - ctx.RenderWithErr(ctx.Tr("install.reinstall_error"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.reinstall_error"), tplInstall, form) return false } @@ -225,7 +225,7 @@ func SubmitInstall(ctx *context.Context) { } if _, err = exec.LookPath("git"); err != nil { - ctx.RenderWithErr(ctx.Tr("install.test_git_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.test_git_failed", err), tplInstall, &form) return } @@ -248,7 +248,7 @@ func SubmitInstall(ctx *context.Context) { // Prepare AppDataPath, it is very important for Gitea if err = setting.PrepareAppDataPath(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.invalid_app_data_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_app_data_path", err), tplInstall, &form) return } @@ -256,7 +256,7 @@ func SubmitInstall(ctx *context.Context) { form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/") if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil { ctx.Data["Err_RepoRootPath"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form) return } @@ -265,7 +265,7 @@ func SubmitInstall(ctx *context.Context) { form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/") if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil { ctx.Data["Err_LFSRootPath"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form) return } } @@ -274,14 +274,14 @@ func SubmitInstall(ctx *context.Context) { form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/") if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil { ctx.Data["Err_LogRootPath"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form) return } currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser) if !match { ctx.Data["Err_RunUser"] = true - ctx.RenderWithErr(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form) return } @@ -289,7 +289,7 @@ func SubmitInstall(ctx *context.Context) { if form.DisableRegistration && len(form.AdminName) == 0 { ctx.Data["Err_Services"] = true ctx.Data["Err_Admin"] = true - ctx.RenderWithErr(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form) return } @@ -300,33 +300,33 @@ func SubmitInstall(ctx *context.Context) { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminName"] = true if db.IsErrNameReserved(err) { - ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form) return } else if db.IsErrNamePatternNotAllowed(err) { - ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form) return } - ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form) return } // Check Admin email if len(form.AdminEmail) == 0 { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminEmail"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_email"), tplInstall, form) return } // Check admin password. if len(form.AdminPasswd) == 0 { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) return } if form.AdminPasswd != form.AdminConfirmPasswd { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplInstall, form) return } } @@ -335,7 +335,7 @@ func SubmitInstall(ctx *context.Context) { if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil { db.UnsetDefaultEngine() ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form) return } @@ -379,7 +379,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath) var lfsJwtSecret string if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form) return } cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret) @@ -389,7 +389,7 @@ func SubmitInstall(ctx *context.Context) { if len(strings.TrimSpace(form.SMTPAddr)) > 0 { if _, err := mail.ParseAddress(form.SMTPFrom); err != nil { - ctx.RenderWithErr(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form) return } @@ -410,7 +410,7 @@ func SubmitInstall(ctx *context.Context) { setting.Config().Picture.DisableGravatar.DynKey(): strconv.FormatBool(form.DisableGravatar), setting.Config().Picture.EnableFederatedAvatar.DynKey(): strconv.FormatBool(form.EnableFederatedAvatar), }); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } @@ -443,7 +443,7 @@ func SubmitInstall(ctx *context.Context) { if setting.InternalToken == "" { var internalToken string if internalToken, err = generate.NewInternalToken(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.internal_token_failed", err), tplInstall, &form) return } cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken) @@ -454,7 +454,7 @@ func SubmitInstall(ctx *context.Context) { if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") { _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() if err != nil { - ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) return } cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64) @@ -464,7 +464,7 @@ func SubmitInstall(ctx *context.Context) { if setting.SecretKey == "" { var secretKey string if secretKey, err = generate.NewSecretKey(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) return } cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey) @@ -474,7 +474,7 @@ func SubmitInstall(ctx *context.Context) { var algorithm *hash.PasswordHashAlgorithm setting.PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(form.PasswordAlgorithm) if algorithm == nil { - ctx.RenderWithErr(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form) return } cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm) @@ -484,14 +484,14 @@ func SubmitInstall(ctx *context.Context) { err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm) if err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } setting.EnvironmentToConfig(cfg, os.Environ()) if err = cfg.SaveTo(setting.CustomConf); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } @@ -527,7 +527,7 @@ func SubmitInstall(ctx *context.Context) { setting.InstallLock = false ctx.Data["Err_AdminName"] = true ctx.Data["Err_AdminEmail"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form) return } log.Info("Admin account already exist") @@ -544,16 +544,16 @@ func SubmitInstall(ctx *context.Context) { // Auto-login for admin if err = ctx.Session.Set("uid", u.ID); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } if err = ctx.Session.Set("uname", u.Name); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } if err = ctx.Session.Release(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } } diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 3407789f2f..e29aca127c 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -273,7 +273,7 @@ func NewAuthSourcePost(ctx *context.Context) { discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { ctx.Data["Err_DiscoveryURL"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form) return } } @@ -281,13 +281,13 @@ func NewAuthSourcePost(ctx *context.Context) { var err error config, err = parseSSPIConfig(ctx, form) if err != nil { - ctx.RenderWithErr(err.Error(), tplAuthNew, form) + ctx.RenderWithErrDeprecated(err.Error(), tplAuthNew, form) return } existing, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{LoginType: auth.SSPI}) if err != nil || len(existing) > 0 { ctx.Data["Err_Type"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form) return } default: @@ -311,11 +311,11 @@ func NewAuthSourcePost(ctx *context.Context) { }); err != nil { if auth.IsErrSourceAlreadyExist(err) { ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form) } else if oauth2.IsErrOpenIDConnectInitialize(err) { ctx.Data["Err_DiscoveryURL"] = true unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap() - ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form) } else { ctx.ServerError("auth.CreateSource", err) } @@ -403,14 +403,14 @@ func EditAuthSourcePost(ctx *context.Context) { discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { ctx.Data["Err_DiscoveryURL"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form) return } } case auth.SSPI: config, err = parseSSPIConfig(ctx, form) if err != nil { - ctx.RenderWithErr(err.Error(), tplAuthEdit, form) + ctx.RenderWithErrDeprecated(err.Error(), tplAuthEdit, form) return } default: @@ -426,7 +426,7 @@ func EditAuthSourcePost(ctx *context.Context) { if err := auth.UpdateSource(ctx, source); err != nil { if auth.IsErrSourceAlreadyExist(err) { ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form) } else if oauth2.IsErrOpenIDConnectInitialize(err) { ctx.Flash.Error(err.Error(), true) ctx.Data["Err_DiscoveryURL"] = true diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index ed0eecf90a..19499fdab5 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -151,12 +151,12 @@ func NewUserPost(ctx *context.Context) { if u.LoginType == auth.NoType || u.LoginType == auth.Plain { if len(form.Password) < setting.MinPasswordLength { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form) return } if !password.IsComplexEnough(form.Password) { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserNew, &form) return } if err := password.IsPwned(ctx, form.Password); err != nil { @@ -166,7 +166,7 @@ func NewUserPost(ctx *context.Context) { log.Error(err.Error()) errMsg = ctx.Tr("auth.password_pwned_err") } - ctx.RenderWithErr(errMsg, tplUserNew, &form) + ctx.RenderWithErrDeprecated(errMsg, tplUserNew, &form) return } u.MustChangePassword = form.MustChangePassword @@ -176,22 +176,22 @@ func NewUserPost(ctx *context.Context) { switch { case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserNew, &form) case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserNew, &form) case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserNew, &form) case db.IsErrNameReserved(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form) case db.IsErrNameCharsNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form) default: ctx.ServerError("CreateUser", err) } @@ -349,19 +349,19 @@ func EditUserPost(ctx *context.Context) { switch { case user_model.IsErrUserIsNotLocal(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form) case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserEdit, &form) case db.IsErrNameReserved(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form) case db.IsErrNameCharsNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form) default: ctx.ServerError("RenameUser", err) } @@ -392,16 +392,16 @@ func EditUserPost(ctx *context.Context) { switch { case errors.Is(err, password.ErrMinLength): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form) case errors.Is(err, password.ErrComplexity): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form) case errors.Is(err, password.ErrIsPwned): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form) case password.IsErrIsPwnedRequest(err): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form) default: ctx.ServerError("UpdateUser", err) } @@ -413,10 +413,10 @@ func EditUserPost(ctx *context.Context) { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserEdit, &form) case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserEdit, &form) default: ctx.ServerError("AddOrSetPrimaryEmailAddress", err) } @@ -444,7 +444,7 @@ func EditUserPost(ctx *context.Context) { if err := user_service.UpdateUser(ctx, u, opts); err != nil { if user_model.IsErrDeleteLastAdminUser(err) { - ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.last_admin"), tplUserEdit, &form) } else { ctx.ServerError("UpdateUser", err) } diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go index a19c9d7aca..73b218d92d 100644 --- a/routers/web/auth/2fa.go +++ b/routers/web/auth/2fa.go @@ -92,7 +92,7 @@ func TwoFactorPost(ctx *context.Context) { return } - ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{}) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{}) } // TwoFactorScratch shows the scratch code form for two-factor authentication. @@ -160,5 +160,5 @@ func TwoFactorScratchPost(ctx *context.Context) { return } - ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{}) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{}) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 9529525a27..c50a197f51 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -245,10 +245,10 @@ func SignInPost(ctx *context.Context) { u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password) if err != nil { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) } else if user_model.IsErrEmailAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplSignIn, &form) log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) } else if user_model.IsErrUserProhibitLogin(err) { log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) @@ -491,23 +491,23 @@ func SignUpPost(ctx *context.Context) { } if !form.IsEmailDomainAllowed() { - ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form) return } if form.Password != form.Retype { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplSignUp, &form) return } if len(form.Password) < setting.MinPasswordLength { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form) return } if !password.IsComplexEnough(form.Password) { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplSignUp, &form) return } if err := password.IsPwned(ctx, form.Password); err != nil { @@ -517,7 +517,7 @@ func SignUpPost(ctx *context.Context) { errMsg = ctx.Tr("auth.password_pwned_err") } ctx.Data["Err_Password"] = true - ctx.RenderWithErr(errMsg, tplSignUp, &form) + ctx.RenderWithErrDeprecated(errMsg, tplSignUp, &form) return } @@ -587,25 +587,25 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, switch { case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tpl, form) case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tpl, form) case user_model.IsErrEmailCharIsNotSupported(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form) case user_model.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form) case db.IsErrNameReserved(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) case db.IsErrNameCharsNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form) default: ctx.ServerError("CreateUser", err) } diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index c624d896ca..faa712471f 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -99,10 +99,10 @@ func LinkAccount(ctx *context.Context) { func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl templates.TplName, invoker string, err error) { if errors.Is(err, util.ErrNotExist) { - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) } else if errors.Is(err, util.ErrInvalidArgument) { ctx.Data["user_exists"] = true - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) } else if user_model.IsErrUserProhibitLogin(err) { ctx.Data["user_exists"] = true log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err) @@ -266,7 +266,7 @@ func LinkAccountPostRegister(ctx *context.Context) { } if !form.IsEmailDomainAllowed() { - ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form) return } @@ -280,12 +280,12 @@ func LinkAccountPostRegister(ctx *context.Context) { } else { if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplLinkAccount, &form) return } if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form) return } } diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 948e65366e..c9843146d4 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -82,7 +82,7 @@ func SignInOpenIDPost(ctx *context.Context) { id, err := openid.Normalize(form.Openid) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form) return } form.Openid = id @@ -91,7 +91,7 @@ func SignInOpenIDPost(ctx *context.Context) { err = allowedOpenIDURI(id) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form) return } @@ -99,7 +99,7 @@ func SignInOpenIDPost(ctx *context.Context) { url, err := openid.RedirectURL(id, redirectTo, setting.AppURL) if err != nil { log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error()) - ctx.RenderWithErr("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form) + ctx.RenderWithErrDeprecated("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form) return } @@ -129,7 +129,7 @@ func signInOpenIDVerify(ctx *context.Context) { id, err := openid.Verify(fullURL) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -143,7 +143,7 @@ func signInOpenIDVerify(ctx *context.Context) { u, err := user_model.GetUserByOpenID(ctx, id) if err != nil { if !user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -162,14 +162,14 @@ func signInOpenIDVerify(ctx *context.Context) { parsedURL, err := url.Parse(fullURL) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return } values, err := url.ParseQuery(parsedURL.RawQuery) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -183,7 +183,7 @@ func signInOpenIDVerify(ctx *context.Context) { u, err = user_model.GetUserByEmail(ctx, email) if err != nil { if !user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -199,7 +199,7 @@ func signInOpenIDVerify(ctx *context.Context) { u, _ = user_model.GetUserByName(ctx, nickname) if err != nil { if !user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -273,7 +273,7 @@ func ConnectOpenIDPost(ctx *context.Context) { userOID := &user_model.UserOpenID{UID: u.ID, URI: oid} if err = user_model.AddUserOpenID(ctx, userOID); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form) return } ctx.ServerError("AddUserOpenID", err) @@ -352,7 +352,7 @@ func RegisterOpenIDPost(ctx *context.Context) { length := max(setting.MinPasswordLength, 256) password, err := util.CryptoRandomString(int64(length)) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignUpOID, form) + ctx.RenderWithErrDeprecated(err.Error(), tplSignUpOID, form) return } @@ -370,7 +370,7 @@ func RegisterOpenIDPost(ctx *context.Context) { userOID := &user_model.UserOpenID{UID: u.ID, URI: oid} if err = user_model.AddUserOpenID(ctx, userOID); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form) return } ctx.ServerError("AddUserOpenID", err) diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index 61c6119470..11e085f5b1 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -74,7 +74,7 @@ func ForgotPasswdPost(ctx *context.Context) { if !u.IsLocal() && !u.IsOAuth2() { ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil) return } @@ -171,7 +171,7 @@ func ResetPasswdPost(ctx *context.Context) { if !twofa.VerifyScratchToken(ctx.FormString("token")) { ctx.Data["IsResetForm"] = true ctx.Data["Err_Token"] = true - ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil) return } regenerateScratchToken = true @@ -185,7 +185,7 @@ func ResetPasswdPost(ctx *context.Context) { if !ok || twofa.LastUsedPasscode == passcode { ctx.Data["IsResetForm"] = true ctx.Data["Err_Passcode"] = true - ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil) return } @@ -206,13 +206,13 @@ func ResetPasswdPost(ctx *context.Context) { ctx.Data["Err_Password"] = true switch { case errors.Is(err, password.ErrMinLength): - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil) case errors.Is(err, password.ErrComplexity): - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil) case errors.Is(err, password.ErrIsPwned): - ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil) case password.IsErrIsPwnedRequest(err): - ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil) default: ctx.ServerError("UpdateAuth", err) } @@ -275,7 +275,7 @@ func MustChangePasswordPost(ctx *context.Context) { if form.Password != form.Retype { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form) return } @@ -287,16 +287,16 @@ func MustChangePasswordPost(ctx *context.Context) { switch { case errors.Is(err, password.ErrMinLength): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form) case errors.Is(err, password.ErrComplexity): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form) case errors.Is(err, password.ErrIsPwned): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form) case password.IsErrIsPwnedRequest(err): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form) default: ctx.ServerError("UpdateAuth", err) } diff --git a/routers/web/org/org.go b/routers/web/org/org.go index 0540d5c591..36978ba65b 100644 --- a/routers/web/org/org.go +++ b/routers/web/org/org.go @@ -65,13 +65,13 @@ func CreatePost(ctx *context.Context) { ctx.Data["Err_OrgName"] = true switch { case user_model.IsErrUserAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form) case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form) case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form) case organization.IsErrUserNotAllowedCreateOrg(err): - ctx.RenderWithErr(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form) default: ctx.ServerError("CreateOrganization", err) } diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 0e4dab8fb6..04baa58b73 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -73,7 +73,7 @@ func SettingsPost(ctx *context.Context) { if form.Email != "" { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) return } } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 0ec7cfddc5..1e22a67032 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -359,7 +359,7 @@ func NewTeamPost(ctx *context.Context) { } if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { - ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) return } @@ -367,7 +367,7 @@ func NewTeamPost(ctx *context.Context) { ctx.Data["Err_TeamName"] = true switch { case org_model.IsErrTeamAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("NewTeam", err) } @@ -536,7 +536,7 @@ func EditTeamPost(ctx *context.Context) { } if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { - ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) return } @@ -544,7 +544,7 @@ func EditTeamPost(ctx *context.Context) { ctx.Data["Err_TeamName"] = true switch { case org_model.IsErrTeamAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("UpdateTeam", err) } diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go index 8f4adb2ad2..bb6f1e6b7e 100644 --- a/routers/web/repo/migrate.go +++ b/routers/web/repo/migrate.go @@ -79,44 +79,44 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, switch { case migrations.IsRateLimitError(err): - ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.visit_rate_limit"), tpl, form) case migrations.IsTwoFactorAuthError(err): - ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.2fa_auth_required"), tpl, form) case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tpl, form) + ctx.RenderWithErrDeprecated(msg, tpl, form) case repo_model.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, form) case repo_model.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form) } case db.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) default: err = util.SanitizeErrorCredentialURLs(err) if strings.Contains(err.Error(), "Authentication failed") || strings.Contains(err.Error(), "Bad credentials") || strings.Contains(err.Error(), "could not read Username") { ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.auth_failed", err.Error()), tpl, form) } else if strings.Contains(err.Error(), "fatal:") { ctx.Data["Err_CloneAddr"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) } else { ctx.ServerError(name, err) } @@ -128,24 +128,24 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl templates addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: - ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form) case addrErr.IsURLError: - ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tpl, form) case addrErr.IsPermissionDenied: if addrErr.LocalPath { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tpl, form) } else { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form) } case addrErr.IsInvalidPath: - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form) default: log.Error("Error whilst updating url: %v", err) - ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form) } } else { log.Error("Error whilst updating url: %v", err) - ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form) } } @@ -193,7 +193,7 @@ func MigratePost(ctx *context.Context) { ep := lfs.DetermineEndpoint("", form.LFSEndpoint) if ep == nil { ctx.Data["Err_LFSEndpoint"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form) return } err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index dd53b1d3f1..09196d4bc2 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -118,7 +118,7 @@ func NewMilestonePost(ctx *context.Context) { deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) if err != nil { ctx.Data["Err_Deadline"] = true - ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } @@ -174,7 +174,7 @@ func EditMilestonePost(ctx *context.Context) { deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) if err != nil { ctx.Data["Err_Deadline"] = true - ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 1b36dc4d44..891af4c2d5 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -452,13 +452,13 @@ func NewReleasePost(ctx *context.Context) { } if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Target); !exist { - ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) return } if !form.TagOnly && form.Title == "" { // if not "tag only", then the title of the release cannot be empty - ctx.RenderWithErr(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form) return } @@ -466,13 +466,13 @@ func NewReleasePost(ctx *context.Context) { ctx.Data["Err_TagName"] = true switch { case release_service.IsErrTagAlreadyExists(err): - ctx.RenderWithErr(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form) case repo_model.IsErrReleaseAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) case release_service.IsErrInvalidTagName(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) case release_service.IsErrProtectedTagName(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) default: ctx.ServerError("handleTagReleaseError", err) } @@ -525,7 +525,7 @@ func NewReleasePost(ctx *context.Context) { // add new logic: if tag-only, do not convert the tag to a release if form.TagOnly || !rel.IsTag { ctx.Data["Err_TagName"] = true - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) return } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index bc2b0264c0..6ab9bc03fb 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -186,28 +186,28 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error, case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tpl, form) + ctx.RenderWithErrDeprecated(msg, tpl, form) case repo_model.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, form) case repo_model.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form) } case db.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) default: ctx.ServerError(name, err) } @@ -254,7 +254,7 @@ func CreatePost(ctx *context.Context) { } if !opts.IsValid() { - ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.one_item"), tplCreate, form) return } @@ -264,7 +264,7 @@ func CreatePost(ctx *context.Context) { } if !templateRepo.IsTemplate { - ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.invalid"), tplCreate, form) return } diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go index 193562528b..ab54b8ccf5 100644 --- a/routers/web/repo/setting/deploy_key.go +++ b/routers/web/repo/setting/deploy_key.go @@ -76,16 +76,16 @@ func DeployKeysPost(ctx *context.Context) { switch { case asymkey_model.IsErrDeployKeyAlreadyExist(err): ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form) case asymkey_model.IsErrKeyAlreadyExist(err): ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form) case asymkey_model.IsErrKeyNameAlreadyUsed(err): ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err): ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) default: ctx.ServerError("AddDeployKey", err) } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index f9e80a72e0..8475c8e21e 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -181,23 +181,23 @@ func handleSettingsPostUpdate(ctx *context.Context) { ctx.Data["Err_RepoName"] = true switch { case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) case repo_model.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) } case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) default: ctx.ServerError("ChangeRepositoryName", err) } @@ -247,7 +247,7 @@ func handleSettingsPostMirror(ctx *context.Context) { interval, err := time.ParseDuration(form.Interval) if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { ctx.Data["Err_Interval"] = true - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) return } @@ -298,7 +298,7 @@ func handleSettingsPostMirror(ctx *context.Context) { ep := lfs.DetermineEndpoint("", form.LFSEndpoint) if ep == nil { ctx.Data["Err_LFSEndpoint"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form) return } err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) @@ -369,7 +369,7 @@ func handleSettingsPostPushMirrorUpdate(ctx *context.Context) { interval, err := time.ParseDuration(form.PushMirrorInterval) if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) return } @@ -445,7 +445,7 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) { interval, err := time.ParseDuration(form.PushMirrorInterval) if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { ctx.Data["Err_PushMirrorInterval"] = true - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) return } @@ -733,7 +733,7 @@ func handleSettingsPostConvert(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -767,7 +767,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -803,14 +803,14 @@ func handleSettingsPostTransfer(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) return } ctx.ServerError("IsUserExist", err) @@ -820,7 +820,7 @@ func handleSettingsPostTransfer(ctx *context.Context) { if newOwner.Type == user_model.UserTypeOrganization { if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { // The user shouldn't know about this organization - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) return } } @@ -834,14 +834,14 @@ func handleSettingsPostTransfer(ctx *context.Context) { oldFullname := repo.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { if repo_model.IsErrRepoAlreadyExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) } else if repo_model.IsErrRepoTransferInProgress(err) { - ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) } else if repo_service.IsRepositoryLimitReached(err) { limit := err.(repo_service.LimitReachedError).Limit - ctx.RenderWithErr(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) } else { ctx.ServerError("TransferOwnership", err) } @@ -895,7 +895,7 @@ func handleSettingsPostDelete(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -922,7 +922,7 @@ func handleSettingsPostDeleteWiki(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -1011,7 +1011,7 @@ func handleSettingsPostVisibility(ctx *context.Context) { // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin { - ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) return } @@ -1039,21 +1039,21 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: - ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form) case addrErr.IsURLError: - ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form) case addrErr.IsPermissionDenied: if addrErr.LocalPath { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form) } else { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form) } case addrErr.IsInvalidPath: - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form) default: ctx.ServerError("Unknown error", err) } return } - ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form) } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 5f775efb22..33f4f7b77b 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -668,7 +668,7 @@ func NewWikiPost(ctx *context.Context) { } if util.IsEmptyString(form.Title) { - ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form) return } @@ -681,10 +681,10 @@ func NewWikiPost(ctx *context.Context) { if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil { if repo_model.IsErrWikiReservedName(err) { ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form) } else if repo_model.IsErrWikiAlreadyExist(err) { ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form) } else { ctx.ServerError("AddWikiPage", err) } diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 2a6c1f00bc..b333f36462 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -186,11 +186,11 @@ func EmailPost(ctx *context.Context) { if user_model.IsErrEmailAlreadyUsed(err) { loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) { loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form) } else { ctx.ServerError("AddEmailAddresses", err) } @@ -251,19 +251,19 @@ func DeleteAccount(ctx *context.Context) { case user_model.IsErrUserNotExist(err): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil) case errors.Is(err, smtp.ErrUnsupportedLoginType): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil) case errors.As(err, &db.ErrUserPasswordNotSet{}): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.unset_password"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.unset_password"), tplSettingsAccount, nil) case errors.As(err, &db.ErrUserPasswordInvalid{}): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) default: ctx.ServerError("UserSignIn", err) } diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 999bb76683..b78a0ec434 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -77,7 +77,7 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form) default: ctx.ServerError("AddPrincipalKey", err) } @@ -108,7 +108,7 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) case asymkey_model.IsErrGPGInvalidTokenSignature(err): loadKeysData(ctx) ctx.Data["Err_Content"] = true @@ -116,7 +116,7 @@ func KeysPost(ctx *context.Context) { keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID ctx.Data["KeyID"] = keyID ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) - ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) case asymkey_model.IsErrGPGNoEmailFound(err): loadKeysData(ctx) @@ -125,7 +125,7 @@ func KeysPost(ctx *context.Context) { keyID := err.(asymkey_model.ErrGPGNoEmailFound).ID ctx.Data["KeyID"] = keyID ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) - ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) default: ctx.ServerError("AddPublicKey", err) } @@ -159,7 +159,7 @@ func KeysPost(ctx *context.Context) { keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID ctx.Data["KeyID"] = keyID ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) - ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) default: ctx.ServerError("VerifyGPG", err) } @@ -194,12 +194,12 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) case asymkey_model.IsErrKeyNameAlreadyUsed(err): loadKeysData(ctx) ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) case asymkey_model.IsErrKeyUnableVerify(err): ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") @@ -230,7 +230,7 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Signature"] = true ctx.Data["Fingerprint"] = err.(asymkey_model.ErrSSHInvalidTokenSignature).Fingerprint - ctx.RenderWithErr(ctx.Tr("settings.ssh_invalid_token_signature"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_invalid_token_signature"), tplSettingsKeys, &form) default: ctx.ServerError("VerifySSH", err) } diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go index 78db7650fe..f71bd518e3 100644 --- a/routers/web/user/setting/security/openid.go +++ b/routers/web/user/setting/security/openid.go @@ -45,7 +45,7 @@ func OpenIDPost(ctx *context.Context) { if err != nil { loadSecurityData(ctx) - ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form) return } form.Openid = id @@ -63,7 +63,7 @@ func OpenIDPost(ctx *context.Context) { if obj.URI == id { loadSecurityData(ctx) - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form) return } } @@ -73,7 +73,7 @@ func OpenIDPost(ctx *context.Context) { if err != nil { loadSecurityData(ctx) - ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form) return } ctx.Redirect(url) @@ -87,7 +87,7 @@ func settingsOpenIDVerify(ctx *context.Context) { id, err := openid.Verify(fullURL) if err != nil { - ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{ Openid: id, }) return @@ -98,7 +98,7 @@ func settingsOpenIDVerify(ctx *context.Context) { oid := &user_model.UserOpenID{UID: ctx.Doer.ID, URI: id} if err = user_model.AddUserOpenID(ctx, oid); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id}) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id}) return } ctx.ServerError("AddUserOpenID", err) diff --git a/services/context/captcha.go b/services/context/captcha.go index b4c3a92907..79278180b7 100644 --- a/services/context/captcha.go +++ b/services/context/captcha.go @@ -98,6 +98,6 @@ func VerifyCaptcha(ctx *Context, tpl templates.TplName, form any) { if !valid { ctx.Data["Err_Captcha"] = true - ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.captcha_incorrect"), tpl, form) } } diff --git a/services/context/context_response.go b/services/context/context_response.go index bb896024b1..d057f8d41e 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -124,8 +124,12 @@ func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTM return template.HTML(buf.String()), err } -// RenderWithErr used for page has form validation but need to prompt error to users. -func (ctx *Context) RenderWithErr(msg any, tpl templates.TplName, form any) { +// RenderWithErrDeprecated render the page with form validation when it needs to prompt error to users. +// Deprecated: use "form-fetch-action" and JSON response instead. +// WARNING: in many cases, this function is not able to render the page or recover the form fields correctly. +// And it is very difficult to test the page rendered by this function. +// DO NOT USE IT ANYMORE. +func (ctx *Context) RenderWithErrDeprecated(msg any, tpl templates.TplName, form any) { if form != nil { middleware.AssignForm(form, ctx.Data) } From fde7f7db285ed4940ff9bb46b5960602797ff7b0 Mon Sep 17 00:00:00 2001 From: James Robinson Date: Fri, 27 Feb 2026 14:10:01 +0000 Subject: [PATCH 020/207] feat: add branch_count to repository API (#35351) (#36743) Description This PR adds a branch_count field to the repository API response. Currently, clients have to fetch all branches via /branches just to determine the total number of branches. This addition brings Gitea closer to parity with GitLab's API and improves efficiency for UI/CLI clients that need this metric. Linked Issue Fixes #35351 Changes API Structs: Added BranchCount field to Repository struct in modules/structs/repo.go. Database Logic: Implemented CountBranches in models/git/branch.go using XORM for efficient counting. Service Layer: Updated the ToRepo conversion logic in services/convert/repository.go to populate the new field during API serialisation. Tests: Added a new unit test TestCountBranches in models/git/branch_test.go to verify counts (including handling of deleted branches). Screenshots Screenshot 2026-02-24 at 21 41 07 Testing Manually verified the output using curl against a local Gitea instance. Verified that adding a branch increments the count and deleting a branch (soft-delete) decrements it. Ran backend linting: make lint-backend (Passed). Ran specific unit test: go test -v -tags "sqlite sqlite_unlock_notify" ./models/git -run TestCountBranches (Passed). Co-authored-by: silverwind --- models/git/branch.go | 9 +++++++++ models/git/branch_test.go | 22 ++++++++++++++++++++++ modules/structs/repo.go | 1 + services/convert/repository.go | 7 +++++++ templates/swagger/v1_json.tmpl | 5 +++++ 5 files changed, 44 insertions(+) diff --git a/models/git/branch.go b/models/git/branch.go index bc6e2a1748..30d517a06d 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -583,3 +583,12 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o return newBranches, nil } + +// CountBranches returns the number of branches in the repository +func CountBranches(ctx context.Context, repoID int64, includeDeleted bool) (int64, error) { + sess := db.GetEngine(ctx).Where("repo_id=?", repoID) + if !includeDeleted { + sess.And("is_deleted=?", false) + } + return sess.Count(new(Branch)) +} diff --git a/models/git/branch_test.go b/models/git/branch_test.go index 9e6148946d..b9b761fd5f 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -263,3 +263,25 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, deletedBranch) } + +func TestCountBranches(t *testing.T) { + // 1. Setup - Exactly like TestAddDeletedBranch + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + // 2. Execution - Using t.Context() to match the rest of the file + initialCount, err := git_model.CountBranches(t.Context(), repo.ID, false) + assert.NoError(t, err) + + // 3. Database Action - Using t.Context() + err = db.Insert(t.Context(), &git_model.Branch{ + RepoID: repo.ID, + Name: "test-branch-for-counting", + }) + assert.NoError(t, err) + + // 4. Verification + newCount, err := git_model.CountBranches(t.Context(), repo.ID, false) + assert.NoError(t, err) + assert.Equal(t, initialCount+1, newCount) +} diff --git a/modules/structs/repo.go b/modules/structs/repo.go index a08cf36037..3507cc410a 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -73,6 +73,7 @@ type Repository struct { Stars int `json:"stars_count"` Forks int `json:"forks_count"` Watchers int `json:"watchers_count"` + BranchCount int `json:"branch_count"` OpenIssues int `json:"open_issues_count"` OpenPulls int `json:"open_pr_counter"` Releases int `json:"release_counter"` diff --git a/services/convert/repository.go b/services/convert/repository.go index 150c952b15..658d31d55c 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -8,6 +8,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -144,6 +145,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR RepoID: repo.ID, }) + branchCount, err := git_model.CountBranches(ctx, repo.ID, false) + if err != nil { + log.Error("CountBranches [%d]: %v", repo.ID, err) + } + mirrorInterval := "" var mirrorUpdated time.Time if repo.IsMirror { @@ -205,6 +211,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR Stars: repo.NumStars, Forks: repo.NumForks, Watchers: repo.NumWatches, + BranchCount: int(branchCount), OpenIssues: repo.NumOpenIssues, OpenPulls: repo.NumOpenPulls, Releases: int(numReleases), diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 7b86cc3d45..212046f8e6 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -28128,6 +28128,11 @@ "type": "string", "x-go-name": "AvatarURL" }, + "branch_count": { + "type": "integer", + "format": "int64", + "x-go-name": "BranchCount" + }, "clone_url": { "type": "string", "x-go-name": "CloneURL" From ae2b19849d2742088f5cdd9a88e1bf6a4b89dd32 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Feb 2026 00:39:26 +0800 Subject: [PATCH 021/207] Use "Enable Gravatar" but not "Disable" (#36771) * Fix #35685 * Fix #35627 * Fix #31112 Introduce "fipped" config value type, remove unused setting variables. Make DisableGravatar=true by defult, remove useless config options from the "Install" page. The legacy config options are still kept because they are still the fallback values for the system config options. --------- Signed-off-by: wxiaoguang --- custom/conf/app.example.ini | 7 ++--- models/user/avatar.go | 2 +- modules/setting/config.go | 2 +- modules/setting/config/value.go | 2 +- modules/setting/picture.go | 25 +++--------------- modules/setting/server.go | 2 -- options/locale/locale_en-US.json | 9 +------ routers/install/install.go | 15 ----------- routers/web/admin/config.go | 1 - services/forms/user_form.go | 3 --- templates/admin/config.tmpl | 2 -- templates/admin/config_settings/avatars.tmpl | 12 ++++++--- templates/install.tmpl | 18 ------------- tests/mssql.ini.tmpl | 5 ---- tests/mysql.ini.tmpl | 5 ---- tests/pgsql.ini.tmpl | 5 ---- tests/sqlite.ini.tmpl | 5 ---- web_src/js/features/admin/config.test.ts | 7 +++++ web_src/js/features/admin/config.ts | 27 ++++++++++++++------ web_src/js/features/install.ts | 19 -------------- 20 files changed, 43 insertions(+), 130 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index f5e8f03ae2..adf78ad0f8 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -238,9 +238,6 @@ RUN_USER = ; git ;; Indicate whether to check minimum key size with corresponding type ;MINIMUM_KEY_SIZE_CHECK = false ;; -;; Disable CDN even in "prod" mode -;OFFLINE_MODE = true -;; ;; TLS Settings: Either ACME or manual ;; (Other common TLS configuration are found before) ;ENABLE_ACME = false @@ -1983,12 +1980,12 @@ LEVEL = Info ;; or a custom avatar source, like: http://cn.gravatar.com/avatar/ ;GRAVATAR_SOURCE = gravatar ;; -;; This value will always be true in offline mode. +;; Deprecated, see Web UI Admin Panel -> Config -> Settings ;DISABLE_GRAVATAR = false ;; ;; Federated avatar lookup uses DNS to discover avatar associated ;; with emails, see https://www.libravatar.org -;; This value will always be false in offline mode or when Gravatar is disabled. +;; Deprecated, see Web UI Admin Panel -> Config -> Settings ;ENABLE_FEDERATED_AVATAR = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/user/avatar.go b/models/user/avatar.go index 542bd93b98..8a5ff30bdf 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -74,7 +74,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { switch { case u.UseCustomAvatar: useLocalAvatar = true - case disableGravatar, setting.OfflineMode: + case disableGravatar: useLocalAvatar = true autoGenerateAvatar = true } diff --git a/modules/setting/config.go b/modules/setting/config.go index bde8e4ac2a..a41734a843 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -71,7 +71,7 @@ func initDefaultConfig() { config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) defaultConfig = &ConfigStruct{ Picture: &PictureStruct{ - DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), + DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithDefaultSimple(true).WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), EnableFederatedAvatar: config.NewOption[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), }, Repository: &RepositoryStruct{ diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go index bd91add97a..0ab04ea656 100644 --- a/modules/setting/config/value.go +++ b/modules/setting/config/value.go @@ -102,7 +102,7 @@ func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool var valStr *string if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue { valStr = &dynVal - } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); has { + } else if cfgVal, hasCfgValue := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); hasCfgValue { valStr = &cfgVal } if valStr == nil { diff --git a/modules/setting/picture.go b/modules/setting/picture.go index fafae45bab..d20a110b6c 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -22,9 +22,7 @@ var ( RenderedSizeFactor: 2, } - GravatarSource string - DisableGravatar bool // Depreciated: migrated to database - EnableFederatedAvatar bool // Depreciated: migrated to database + GravatarSource string RepoAvatar = struct { Storage *Storage @@ -65,29 +63,12 @@ func loadAvatarsFrom(rootCfg ConfigProvider) error { GravatarSource = source } - DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool(GetDefaultDisableGravatar()) - deprecatedSettingDB(rootCfg, "", "DISABLE_GRAVATAR") - EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar)) - deprecatedSettingDB(rootCfg, "", "ENABLE_FEDERATED_AVATAR") + deprecatedSettingDB(rootCfg, "picture", "DISABLE_GRAVATAR") + deprecatedSettingDB(rootCfg, "picture", "ENABLE_FEDERATED_AVATAR") return nil } -func GetDefaultDisableGravatar() bool { - return OfflineMode -} - -func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool { - v := !InstallLock - if OfflineMode { - v = false - } - if disableGravatar { - v = false - } - return v -} - func loadRepoAvatarFrom(rootCfg ConfigProvider) error { sec := rootCfg.Section("picture") diff --git a/modules/setting/server.go b/modules/setting/server.go index a865e942a6..2ea52bc9a5 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -91,7 +91,6 @@ var ( RedirectOtherPort bool RedirectorUseProxyProtocol bool PortToRedirect string - OfflineMode bool CertFile string KeyFile string StaticRootPath string @@ -346,7 +345,6 @@ func loadServerFrom(rootCfg ConfigProvider) { RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false) PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80") RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol) - OfflineMode = sec.Key("OFFLINE_MODE").MustBool(true) if len(StaticRootPath) == 0 { StaticRootPath = AppWorkPath } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index bcd28f2deb..a3dc09bb21 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -285,12 +285,6 @@ "install.register_confirm": "Require Email Confirmation to Register", "install.mail_notify": "Enable Email Notifications", "install.server_service_title": "Server and Third-Party Service Settings", - "install.offline_mode": "Enable Local Mode", - "install.offline_mode_popup": "Disable third-party content delivery networks and serve all resources locally.", - "install.disable_gravatar": "Disable Gravatar", - "install.disable_gravatar_popup": "Disable Gravatar and third-party avatar sources. A default avatar will be used unless a user locally uploads an avatar.", - "install.federated_avatar_lookup": "Enable Federated Avatars", - "install.federated_avatar_lookup_popup": "Enable federated avatar lookup using Libravatar.", "install.disable_registration": "Disable Self-Registration", "install.disable_registration_popup": "Disable user self-registration. Only administrators will be able to create new user accounts.", "install.allow_only_external_registration_popup": "Allow Registration Only Through External Services", @@ -3193,7 +3187,6 @@ "admin.config.custom_conf": "Configuration File Path", "admin.config.custom_file_root_path": "Custom File Root Path", "admin.config.domain": "Server Domain", - "admin.config.offline_mode": "Local Mode", "admin.config.disable_router_log": "Disable Router Log", "admin.config.run_user": "Run As Username", "admin.config.run_mode": "Run Mode", @@ -3296,7 +3289,7 @@ "admin.config.cookie_life_time": "Cookie Life Time", "admin.config.picture_config": "Picture and Avatar Configuration", "admin.config.picture_service": "Picture Service", - "admin.config.disable_gravatar": "Disable Gravatar", + "admin.config.enable_gravatar": "Enable Gravatar", "admin.config.enable_federated_avatar": "Enable Federated Avatars", "admin.config.open_with_editor_app_help": "The \"Open with\" editors for the clone menu. If left empty, the default will be used. Expand to see the default.", "admin.config.git_guide_remote_name": "Repository remote name for git commands in the guide", diff --git a/routers/install/install.go b/routers/install/install.go index 1a60fee339..81fcdfa384 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/models/db" db_install "code.gitea.io/gitea/models/db/install" - system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/generate" @@ -114,11 +113,6 @@ func Install(ctx *context.Context) { form.RegisterConfirm = setting.Service.RegisterEmailConfirm form.MailNotify = setting.Service.EnableNotifyMail - // Server and other services settings - form.OfflineMode = setting.OfflineMode - form.DisableGravatar = setting.DisableGravatar // when installing, there is no database connection so that given a default value - form.EnableFederatedAvatar = setting.EnableFederatedAvatar // when installing, there is no database connection so that given a default value - form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp form.DisableRegistration = setting.Service.DisableRegistration @@ -405,15 +399,6 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(strconv.FormatBool(form.RegisterConfirm)) cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(strconv.FormatBool(form.MailNotify)) - cfg.Section("server").Key("OFFLINE_MODE").SetValue(strconv.FormatBool(form.OfflineMode)) - if err := system_model.SetSettings(ctx, map[string]string{ - setting.Config().Picture.DisableGravatar.DynKey(): strconv.FormatBool(form.DisableGravatar), - setting.Config().Picture.EnableFederatedAvatar.DynKey(): strconv.FormatBool(form.EnableFederatedAvatar), - }); err != nil { - ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) - return - } - cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(strconv.FormatBool(form.EnableOpenIDSignIn)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(strconv.FormatBool(form.EnableOpenIDSignUp)) cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(strconv.FormatBool(form.DisableRegistration)) diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 79e969fd5e..a449796ec1 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -126,7 +126,6 @@ func Config(ctx *context.Context) { ctx.Data["AppUrl"] = setting.AppURL ctx.Data["AppBuiltWith"] = setting.AppBuiltWith ctx.Data["Domain"] = setting.Domain - ctx.Data["OfflineMode"] = setting.OfflineMode ctx.Data["RunUser"] = setting.RunUser ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) ctx.Data["GitVersion"] = git.DefaultFeatures().VersionInfo() diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 618294d434..cc514a2e27 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -45,9 +45,6 @@ type InstallForm struct { RegisterConfirm bool MailNotify bool - OfflineMode bool - DisableGravatar bool - EnableFederatedAvatar bool EnableOpenIDSignIn bool EnableOpenIDSignUp bool DisableRegistration bool diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 728746713c..a61dec9620 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -15,8 +15,6 @@
    {{.AppUrl}}
    {{ctx.Locale.Tr "admin.config.domain"}}
    {{.Domain}}
    -
    {{ctx.Locale.Tr "admin.config.offline_mode"}}
    -
    {{svg (Iif .OfflineMode "octicon-check" "octicon-x")}}
    {{ctx.Locale.Tr "admin.config.disable_router_log"}}
    {{svg (Iif .DisableRouterLog "octicon-check" "octicon-x")}}
    diff --git a/templates/admin/config_settings/avatars.tmpl b/templates/admin/config_settings/avatars.tmpl index 1fc761034d..7a2e40a68e 100644 --- a/templates/admin/config_settings/avatars.tmpl +++ b/templates/admin/config_settings/avatars.tmpl @@ -3,17 +3,21 @@
    -
    {{ctx.Locale.Tr "admin.config.disable_gravatar"}}
    + {{$cfgOpt := .SystemConfig.Picture.DisableGravatar}} +
    {{ctx.Locale.Tr "admin.config.enable_gravatar"}}
    -
    - +
    +
    +
    + + {{$cfgOpt = .SystemConfig.Picture.EnableFederatedAvatar}}
    {{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}
    - +
    diff --git a/templates/install.tmpl b/templates/install.tmpl index acfafd3cc7..45f14d5c57 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -203,24 +203,6 @@ {{ctx.Locale.Tr "install.server_service_title"}} -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl index 0d16905033..6f031691f0 100644 --- a/tests/mssql.ini.tmpl +++ b/tests/mssql.ini.tmpl @@ -41,7 +41,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2201 START_SSH_SERVER = true LFS_START_SERVER = true -OFFLINE_MODE = false LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w BUILTIN_SSH_SERVER_USER = git SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= @@ -62,10 +61,6 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org ENABLE_NOTIFY_MAIL = true -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl index bf59efde4c..02069b403e 100644 --- a/tests/mysql.ini.tmpl +++ b/tests/mysql.ini.tmpl @@ -43,7 +43,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2201 BUILTIN_SSH_SERVER_USER = git START_SSH_SERVER = true -OFFLINE_MODE = false LFS_START_SERVER = true LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w @@ -65,10 +64,6 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org ENABLE_NOTIFY_MAIL = true -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index b6fcd33f70..50a649fc79 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -42,7 +42,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2202 START_SSH_SERVER = true LFS_START_SERVER = true -OFFLINE_MODE = false LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w BUILTIN_SSH_SERVER_USER = git SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= @@ -63,10 +62,6 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org ENABLE_NOTIFY_MAIL = true -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 243bea86f1..b43b658bb0 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -37,7 +37,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2203 START_SSH_SERVER = true LFS_START_SERVER = true -OFFLINE_MODE = false LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w ENABLE_GZIP = true BUILTIN_SSH_SERVER_USER = git @@ -59,10 +58,6 @@ DEFAULT_KEEP_EMAIL_PRIVATE = false DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/web_src/js/features/admin/config.test.ts b/web_src/js/features/admin/config.test.ts index e44ccb2a94..3de524070c 100644 --- a/web_src/js/features/admin/config.test.ts +++ b/web_src/js/features/admin/config.test.ts @@ -7,10 +7,15 @@ test('ConfigFormValueMapper', () => { + + + + + @@ -35,6 +40,8 @@ test('ConfigFormValueMapper', () => { expect(result).toEqual({ 'k1': 'true', 'k2': '"k2-val"', + 'k-flipped-false': 'false', + 'k-flipped-true': 'true', 'repository.open-with.editor-apps': '[{"DisplayName":"a","OpenURL":"b"}]', // TODO: OPEN-WITH-EDITOR-APP-JSON: it must match backend 'struct': '{"SubBoolean":true,"SubTimestamp":123456780,"OtherKey":"other-value","NewKey":"new-value"}', }); diff --git a/web_src/js/features/admin/config.ts b/web_src/js/features/admin/config.ts index 047c1a46a4..5df81412f9 100644 --- a/web_src/js/features/admin/config.ts +++ b/web_src/js/features/admin/config.ts @@ -6,14 +6,23 @@ import {submitFormFetchAction} from '../common-fetch-action.ts'; const {appSubUrl} = window.config; +function collectCheckboxBooleanValue(el: HTMLInputElement): boolean { + const valType = el.getAttribute('data-config-value-type') as ConfigValueType; + if (valType === 'boolean') return el.checked; + if (valType === 'flipped') return !el.checked; + requireExplicitValueType(el); +} + function initSystemConfigAutoCheckbox(el: HTMLInputElement) { el.addEventListener('change', async () => { // if the checkbox is inside a form, we assume it's handled by the form submit and do not send an individual request if (el.closest('form')) return; try { - const resp = await POST(`${appSubUrl}/-/admin/config`, { - data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key')!, value: String(el.checked)}), + const data = new URLSearchParams({ + key: el.getAttribute('data-config-dyn-key')!, + value: String(collectCheckboxBooleanValue(el)), }); + const resp = await POST(`${appSubUrl}/-/admin/config`, {data}); const json: Record = await resp.json(); if (json.errorMessage) throw new Error(json.errorMessage); } catch (ex) { @@ -47,7 +56,7 @@ function extractElemConfigSubKey(el: GeneralFormFieldElement, dynKey: string): s // Due to the different design between HTML form elements and the JSON struct of the config values, we need to explicitly define some types. // * checkbox can be used for boolean value, it can also be used for multiple values (array) -type ConfigValueType = 'boolean' | 'string' | 'number' | 'timestamp'; // TODO: support more types like array, not used at the moment. +type ConfigValueType = 'boolean' | 'flipped' | 'string' | 'number' | 'timestamp'; // TODO: support more types like array, not used at the moment. function toDatetimeLocalValue(unixSeconds: number) { const d = new Date(unixSeconds * 1000); @@ -102,13 +111,14 @@ export class ConfigFormValueMapper { return true; } - collectConfigValueFromElement(el: GeneralFormFieldElement, _oldVal: any = null) { + collectConfigValueFromElement(el: GeneralFormFieldElement) { let val: any; const valType = this.presetValueTypes[el.name]; if (el.matches('[type="checkbox"]')) { - if (valType !== 'boolean') requireExplicitValueType(el); - val = el.checked; - // oldVal: for future use when we support array value with checkbox + // TODO: if it needs to support array values in the future, + // it needs to iterate the "namedElems" to find all the checkboxes with the same name and collect values accordingly, + // and set the namedElems[matchedIdx] to null to avoid duplicate processing. + val = collectCheckboxBooleanValue(el); } else if (el.matches('[type="datetime-local"]')) { if (valType !== 'timestamp') requireExplicitValueType(el); val = Math.floor(new Date(el.value).getTime() / 1000) ?? 0; // NaN is fine to JSON.stringify, it becomes null. @@ -128,7 +138,7 @@ export class ConfigFormValueMapper { if (!el) continue; const subKey = extractElemConfigSubKey(el, dynKey); if (!subKey) continue; // if not match, skip - cfgVal[subKey] = this.collectConfigValueFromElement(el, cfgVal[subKey]); + cfgVal[subKey] = this.collectConfigValueFromElement(el); namedElems[idx] = null; } } @@ -180,6 +190,7 @@ export class ConfigFormValueMapper { // now, the namedElems should only contain the config options without sub values, // directly store the value in formData with key as the element name, for example: + // "foo.enabled" => "true" for (const el of namedElems) { if (!el) continue; const dynKey = el.name; diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index c00fe3c03d..f316cd532d 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -60,25 +60,6 @@ function initPreInstall() { } // TODO: better handling of exclusive relations. - document.querySelector('#offline-mode input')!.addEventListener('change', function () { - if (this.checked) { - document.querySelector('#disable-gravatar input')!.checked = true; - document.querySelector('#federated-avatar-lookup input')!.checked = false; - } - }); - document.querySelector('#disable-gravatar input')!.addEventListener('change', function () { - if (this.checked) { - document.querySelector('#federated-avatar-lookup input')!.checked = false; - } else { - document.querySelector('#offline-mode input')!.checked = false; - } - }); - document.querySelector('#federated-avatar-lookup input')!.addEventListener('change', function () { - if (this.checked) { - document.querySelector('#disable-gravatar input')!.checked = false; - document.querySelector('#offline-mode input')!.checked = false; - } - }); document.querySelector('#enable-openid-signin input')!.addEventListener('change', function () { if (this.checked) { if (!document.querySelector('#disable-registration input')!.checked) { From 50ec48d9fee605f9ffa9210ae8eaddb9530ea840 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Feb 2026 17:45:10 +0100 Subject: [PATCH 022/207] Move Fomantic dropdown CSS to custom module (#36530) Moved fomantic dropdown css to custom module, tested on the dropdown devtest page, it renders exactly the same as before while using roughly 50% less CSS. The clean up was very conservative, likely more can be done in the future. Also, this fixes a bug present on main branch where dropdown border has incorrect color on hover. --------- Signed-off-by: silverwind Co-authored-by: Claude Opus 4.5 --- stylelint.config.js | 2 +- web_src/css/base.css | 237 +-- web_src/css/index.css | 1 + web_src/css/modules/divider.css | 5 - web_src/css/modules/dropdown.css | 958 +++++++++ .../fomantic/build/components/dropdown.css | 1755 ----------------- web_src/fomantic/build/components/dropdown.js | 2 +- web_src/fomantic/build/fomantic.css | 1 - web_src/fomantic/semantic.json | 1 - 9 files changed, 962 insertions(+), 2000 deletions(-) create mode 100644 web_src/css/modules/dropdown.css delete mode 100644 web_src/fomantic/build/components/dropdown.css diff --git a/stylelint.config.js b/stylelint.config.js index 42edf76f43..3e6be3c248 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -125,7 +125,7 @@ export default { 'csstools/value-no-unknown-custom-properties': [true, {importFrom: cssVarFiles}], 'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates-with-different-values']}], 'declaration-block-no-redundant-longhand-properties': [true, {ignoreShorthands: ['flex-flow', 'overflow', 'grid-template']}], - 'declaration-property-unit-disallowed-list': {'line-height': ['em']}, + 'declaration-property-unit-disallowed-list': null, 'declaration-property-value-disallowed-list': {'word-break': ['break-word']}, 'font-family-name-quotes': 'always-where-recommended', 'function-name-case': 'lower', diff --git a/web_src/css/base.css b/web_src/css/base.css index 3fa5c1246c..59072839a9 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -243,9 +243,7 @@ progress::-moz-progress-bar { color: var(--color-white); } -::placeholder, -.ui.dropdown:not(.button) > .default.text, -.ui.default.dropdown:not(.button) > .text { +::placeholder { color: var(--color-placeholder-text) !important; opacity: 1 !important; } @@ -397,83 +395,6 @@ a.label, color: var(--color-text-light-2); } -.ui.dropdown .menu { - background: var(--color-menu); - border-color: var(--color-secondary); -} - -.ui.dropdown .menu > .header { - text-transform: none; /* reset fomantic's "uppercase" */ -} - -.ui.dropdown .menu > .header:not(.ui) { - color: var(--color-text); - font-size: 0.95em; /* reset fomantic's small font-size */ -} - -.ui.dropdown .menu > .item { - color: var(--color-text); - line-height: var(--line-height-default); -} - -.ui.dropdown .menu > .item:hover { - color: var(--color-text); - background: var(--color-hover); -} - -.ui.dropdown .menu > .item:active { - color: var(--color-text); - background: var(--color-active); -} - -.ui.dropdown .menu .active.item { - color: var(--color-text); - background: var(--color-active); - border-radius: 0; - font-weight: var(--font-weight-normal); -} - -/* fix misaligned images in webhook dropdown */ -.ui.dropdown .menu > .item > img { - margin-top: -0.25rem; - margin-bottom: -0.25rem; -} -.ui.dropdown .menu > .item > svg { - margin-right: .78rem; /* use the same margin as for */ -} - -.ui.selection.dropdown .menu > .item { - border-color: var(--color-secondary); -} -.ui.selection.dropdown .menu .item:first-of-type { - border-radius: 0; -} -.ui.selection.visible.dropdown > .text:not(.default) { - color: var(--color-text); -} - -.ui.dropdown.selected, -.ui.dropdown .menu .selected.item { - color: var(--color-text); - background: var(--color-hover); -} - -.ui.dropdown .menu > .message:not(.ui) { - color: var(--color-text-light-2); -} - -/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */ -.ui.dropdown > .text > .img { - margin-left: 0; - float: none; - margin-right: 0.78571429rem; -} - -.ui.dropdown > .text > .description, -.ui.dropdown .menu > .item > .description { - color: var(--color-text-light-2); -} - /* styles from removed fomantic transition module */ .hidden.transition { visibility: hidden; @@ -484,23 +405,6 @@ a.label, visibility: visible !important; } -.ui.selection.active.dropdown, -.ui.selection.active.dropdown:hover, -.ui.selection.active.dropdown .menu, -.ui.selection.active.dropdown:hover .menu { - border-color: var(--color-primary); -} - -.ui.pointing.dropdown > .menu:not(.hidden)::after { - background: var(--color-menu); - box-shadow: -1px -1px 0 0 var(--color-secondary); -} - -.ui.pointing.upward.dropdown .menu::after, -.ui.top.pointing.upward.dropdown .menu::after { - box-shadow: 1px 1px 0 0 var(--color-secondary); -} - .ui.comments .comment .metadata { color: var(--color-text-light-2); } @@ -553,20 +457,6 @@ img.ui.avatar, margin-top: calc(var(--page-spacing) - 1rem); } -/* popover box shadows */ -.ui.dropdown .menu, -.ui.upward.dropdown > .menu, -.ui.menu .dropdown.item .menu, -.ui.selection.active.dropdown .menu, -.ui.upward.selection.dropdown .menu, -.ui.selection.active.dropdown:hover .menu, -.ui.upward.active.selection.dropdown:hover .menu { - box-shadow: 0 6px 18px var(--color-shadow); -} -.ui.floating.dropdown .menu { - box-shadow: 0 6px 18px var(--color-shadow) !important; -} - .ui .message.flash-message { text-align: center; } @@ -618,27 +508,11 @@ img.ui.avatar, border: 1px solid; } -.ui.dropdown .menu.context-user-switch .scrolling.menu { - border-radius: 0 !important; - box-shadow: none !important; - border-bottom: 1px solid var(--color-secondary); - max-width: 80vw; -} - .user-menu > .item { width: 100%; border-radius: 0 !important; } -.scrolling.menu .item.selected { - font-weight: var(--font-weight-semibold) !important; -} - -.ui.dropdown .scrolling.menu { - border-color: var(--color-secondary); - border-radius: 0 0 var(--border-radius) var(--border-radius) !important; -} - .color-preview { display: inline-block; margin-left: 0.4em; @@ -914,22 +788,6 @@ table th[data-sortt-desc] .svg { margin-left: 0.25rem; } -.ui.dropdown .menu .item { - border-radius: 0; -} - -.ui.dropdown .menu .item:first-of-type { - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.ui.dropdown .menu .item:last-of-type { - border-radius: 0 0 var(--border-radius) var(--border-radius); -} - -.ui.multiple.dropdown > .label { - box-shadow: 0 0 0 1px var(--color-secondary) inset; -} - /* for "image" emojis like ":git:" ":gitea:" and ":github:" (see CUSTOM_EMOJIS config option) */ .emoji img { border-width: 0 !important; @@ -978,54 +836,6 @@ table th[data-sortt-desc] .svg { min-height: 0; } -.ui.dropdown:not(.button) { - line-height: var(--line-height-default); /* the dropdown doesn't have default line-height, use this to make the dropdown icon align with plain dropdown */ -} - -/* dropdown has some kinds of icons: -- "> .dropdown.icon": the arrow for opening the dropdown -- "> .remove.icon": the "x" icon for clearing the dropdown, only used in selection dropdown -- "> .ui.label > .delete.icon": the "x" icon for removing a label item in multiple selection dropdown -*/ - -.ui.dropdown.mini.button, -.ui.dropdown.tiny.button { - padding-right: 20px; -} -.ui.dropdown.button { - padding-right: 22px; -} -.ui.dropdown.large.button { - padding-right: 24px; -} - -/* Gitea uses SVG images instead of Fomantic builtin "" font icons, so we need to reset the icon styles */ -.ui.ui.dropdown > .icon.icon { - position: initial; /* plain dropdown and button dropdown use flex layout for icons */ - padding: 0; - margin: 0; - height: auto; -} - -.ui.ui.dropdown > .icon.icon:hover { - opacity: 1; -} - -.ui.ui.button.dropdown > .icon.icon, -.ui.ui.selection.dropdown > .icon.icon { - position: absolute; /* selection dropdown uses absolute layout for icons */ - top: 50%; - transform: translateY(-50%); -} - -.ui.ui.dropdown > .dropdown.icon { - right: 0.5em; -} - -.ui.ui.dropdown > .remove.icon { - right: 2em; -} - .btn, .ui.ui.dropdown, .flex-text-inline, @@ -1038,18 +848,6 @@ table th[data-sortt-desc] .svg { min-width: 0; /* make ellipsis work */ } -.ui.multiple.selection.dropdown { - flex-wrap: wrap; -} - -.ui.ui.dropdown.selection { - min-width: 14em; /* match the default min width */ -} - -.ui.dropdown .ui.label .svg { - vertical-align: middle; -} - .ui.ui.labeled.button { gap: 0; align-items: stretch; @@ -1066,44 +864,11 @@ table th[data-sortt-desc] .svg { min-width: 0; } -.ui.dropdown > .ui.button, .flex-text-block > .ui.button, .flex-text-inline > .ui.button { margin: 0; /* fomantic buttons have default margin, when we use them in a flex container with gap, we do not need these margins */ } -/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content -the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */ -.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) { - display: flex !important; - align-items: center; - gap: var(--gap-block); - min-width: 0; -} -.ui.dropdown .menu.flex-items-menu > .item img, -.ui.dropdown .menu.flex-items-menu > .item svg { - margin: 0; /* use gap, but not margin */ -} - -.ui.dropdown.ellipsis-text-items { - /* reset y padding and use the line-height below instead, to avoid the "overflow: hidden" clips the larger image in the "text" element */ - padding-top: 0; - padding-bottom: 0; -} - -.ui.dropdown.ellipsis-text-items > .text { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - line-height: 2.71; /* matches fomantic dropdown's default min-height */ -} - -.ui.dropdown.ellipsis-text-items .menu > .item { - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; -} - .svg.octicon-file-directory-fill, .svg.octicon-file-directory-open-fill, .svg.octicon-file-submodule { diff --git a/web_src/css/index.css b/web_src/css/index.css index 699ba221ca..3edb3df6c1 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -20,6 +20,7 @@ @import "./modules/modal.css"; @import "./modules/tab.css"; @import "./modules/form.css"; +@import "./modules/dropdown.css"; @import "./modules/shortcut.css"; @import "./modules/tippy.css"; diff --git a/web_src/css/modules/divider.css b/web_src/css/modules/divider.css index acc8408f37..a60b7d52cb 100644 --- a/web_src/css/modules/divider.css +++ b/web_src/css/modules/divider.css @@ -36,8 +36,3 @@ h4.divider { .divider.divider-text::after { margin-left: .75em; } - -.ui.dropdown .menu > .divider { - border-top: 1px solid var(--color-secondary); - margin: 4px 0; -} diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css new file mode 100644 index 0000000000..2008ece2ed --- /dev/null +++ b/web_src/css/modules/dropdown.css @@ -0,0 +1,958 @@ +/* These are the remnants of the fomantic dropdown module */ + +.ui.dropdown { + cursor: pointer; + position: relative; + display: inline-block; + outline: none; + text-align: left; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.ui.dropdown .menu { + cursor: auto; + position: absolute; + display: none; + outline: none; + top: 100%; + min-width: max-content; + margin: 0; + padding: 0; + background: var(--color-menu); + font-size: 1em; + text-align: left; + box-shadow: 0 6px 18px var(--color-shadow); + border: 1px solid var(--color-secondary); + border-radius: 0.28571429rem; + z-index: 11; + left: 0; +} + +.ui.dropdown .menu > * { + white-space: nowrap; +} + +.ui.dropdown > input:not(.search):first-child, +.ui.dropdown > select { + display: none !important; +} + +.ui.dropdown > .dropdown.icon { + line-height: 1; + height: 1em; + width: auto; + backface-visibility: hidden; + text-align: center; +} + +.ui.dropdown:not(.labeled) > .dropdown.icon { + position: relative; + width: auto; + font-size: 0.85714286em; + margin: 0 0 0 1em; +} + +.ui.dropdown > .text { + display: inline-block; +} + +.ui.dropdown .menu > .item { + position: relative; + cursor: pointer; + display: block; + border: none; + height: auto; + min-height: 2.57142857rem; + text-align: left; + border-top: none; + line-height: var(--line-height-default); + font-size: 1rem; + color: var(--color-text); + padding: 0.78571429rem 1.14285714rem !important; + text-transform: none; + font-weight: var(--font-weight-normal); + box-shadow: none; + -webkit-touch-callout: none; +} + +.ui.dropdown .menu > .item:first-child { + border-top-width: 0; +} + +.ui.dropdown .menu > .header { + margin: 1rem 0 0.75rem; + padding: 0 1.14285714rem; + font-weight: var(--font-weight-medium); + text-transform: none; +} + +.ui.dropdown .menu > .header:not(.ui) { + color: var(--color-text); + font-size: 0.95em; +} + +.ui.dropdown .menu > .divider { + border-top: 1px solid var(--color-secondary); + height: 0; + margin: 4px 0; +} + +.ui.dropdown.dropdown .menu > .input { + width: auto; + display: flex; + margin: 1.14285714rem 0.78571429rem; + min-width: 10rem; +} + +.ui.dropdown .menu > .header + .input { + margin-top: 0; +} + +.ui.dropdown .menu > .input:not(.transparent) input { + padding: 0.5em 1em; +} + +.ui.dropdown .menu > .input:not(.transparent) .button, +.ui.dropdown .menu > .input:not(.transparent) i.icon, +.ui.dropdown .menu > .input:not(.transparent) .label { + padding-top: 0.5em; + padding-bottom: 0.5em; +} + +.ui.dropdown > .text > .description, +.ui.dropdown .menu > .item > .description { + float: right; + margin: 0 0 0 1em; + color: var(--color-text-light-2); +} + +.ui.dropdown .menu > .message { + padding: 0.78571429rem 1.14285714rem; + font-weight: var(--font-weight-normal); +} + +.ui.dropdown .menu > .message:not(.ui) { + color: var(--color-text-light-2); +} + +/* Remove Menu Item Divider */ +.ui.dropdown .ui.menu > .item::before, +.ui.menu .ui.dropdown .menu > .item::before { + display: none; +} + +/* Prevent Menu Item Border */ +.ui.menu .ui.dropdown .menu .active.item { + border-left: none; +} + +/* Automatically float dropdown menu right on last menu item */ +.ui.menu .right.menu .dropdown:last-child > .menu:not(.left), +.ui.menu .right.dropdown.item > .menu:not(.left), +.ui.buttons > .ui.dropdown:last-child > .menu:not(.left) { + left: auto; + right: 0; +} + +.ui.button.dropdown .menu { + min-width: 100%; +} + +select.ui.dropdown { + height: 38px; + padding: 0.5em; + border: 1px solid var(--color-input-border); + visibility: visible; +} + +.ui.selection.dropdown { + cursor: pointer; + overflow-wrap: break-word; + line-height: 1em; + white-space: normal; + outline: 0; + transform: rotateZ(0deg); + min-width: 14em; + min-height: 2.71428571em; + background: var(--color-input-background); + display: inline-block; + padding: 0.78571429em 3.2em 0.78571429em 1em; + color: var(--color-input-text); + box-shadow: none; + border: 1px solid var(--color-input-border); + border-radius: 0.28571429rem; +} + +.ui.selection.dropdown.visible, +.ui.selection.dropdown.active { + z-index: 10; +} + +.ui.selection.dropdown > .search.icon, +.ui.selection.dropdown > .delete.icon, +.ui.selection.dropdown > .dropdown.icon { + cursor: pointer; + position: absolute; + width: auto; + height: auto; + line-height: 1.21428571em; + top: 0.78571429em; + right: 1em; + z-index: 3; + margin: -0.78571429em; + padding: 0.91666667em; + opacity: 0.8; +} + +.ui.selection.dropdown .menu { + overflow-x: hidden; + overflow-y: auto; + backface-visibility: hidden; + -webkit-overflow-scrolling: touch; + border-top-width: 0 !important; + outline: none; + margin: 0 -1px; + min-width: calc(100% + 2px); + width: calc(100% + 2px); + border-radius: 0 0 0.28571429rem 0.28571429rem; + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.dropdown .menu::after, +.ui.selection.dropdown .menu::before { + display: none; +} + +.ui.selection.dropdown .menu > .message { + padding: 0.78571429rem 1.14285714rem; +} + +@media only screen and (max-width: 767.98px) { + .ui.selection.dropdown .menu { + max-height: 8.01428571rem; + } +} + +@media only screen and (min-width: 768px) { + .ui.selection.dropdown .menu { + max-height: 10.68571429rem; + } +} + +@media only screen and (min-width: 992px) { + .ui.selection.dropdown .menu { + max-height: 16.02857143rem; + } +} + +@media only screen and (min-width: 1920px) { + .ui.selection.dropdown .menu { + max-height: 21.37142857rem; + } +} + +.ui.selection.dropdown .menu > .item { + border-top: 1px solid var(--color-secondary); + padding: 0.78571429rem 1.14285714rem !important; + white-space: normal; + overflow-wrap: normal; +} + +.ui.selection.dropdown .menu .item:first-of-type { + border-radius: 0; +} + +.ui.selection.dropdown .menu > .hidden.addition.item { + display: none; +} + +.ui.selection.dropdown:hover { + border-color: var(--color-input-border-hover); + box-shadow: none; +} + +.ui.selection.active.dropdown { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.active.dropdown .menu { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.dropdown:focus { + border-color: var(--color-primary); + box-shadow: none; +} + +.ui.selection.dropdown:focus .menu { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.visible.dropdown > .text:not(.default) { + font-weight: var(--font-weight-normal); + color: var(--color-text); +} + +.ui.selection.active.dropdown:hover { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.active.dropdown:hover .menu { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.active.selection.dropdown > .dropdown.icon, +.ui.visible.selection.dropdown > .dropdown.icon { + z-index: 3; +} + +.ui.active.selection.dropdown { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.ui.active.empty.selection.dropdown { + border-radius: 0.28571429rem !important; + box-shadow: none !important; +} + +.ui.active.empty.selection.dropdown .menu { + border: none !important; + box-shadow: none !important; +} + +.ui.search.dropdown > input.search { + background: none transparent !important; + border: none !important; + box-shadow: none !important; + cursor: text; + top: 0; + left: 1px; + width: 100%; + outline: none; + -webkit-tap-highlight-color: transparent; + padding: inherit; + position: absolute; + z-index: 2; +} + +.ui.search.dropdown > .text { + cursor: text; + position: relative; + left: 1px; + z-index: auto; +} + +.ui.search.selection.dropdown > input.search { + line-height: 1.21428571em; + padding: 0.67857143em 3.2em 0.67857143em 1em; +} + +.ui.search.selection.dropdown > span.sizer { + line-height: 1.21428571em; + padding: 0.67857143em 3.2em 0.67857143em 1em; + display: none; + white-space: pre; +} + +.ui.search.dropdown.active > input.search, +.ui.search.dropdown.visible > input.search { + cursor: auto; +} + +.ui.search.dropdown.active > .text, +.ui.search.dropdown.visible > .text { + pointer-events: none; +} + +.ui.active.search.dropdown input.search:focus + .text i.icon { + opacity: var(--opacity-disabled); +} + +.ui.active.search.dropdown input.search:focus + .text { + color: var(--color-placeholder-text) !important; +} + +.ui.search.dropdown .menu { + overflow-x: hidden; + overflow-y: auto; + backface-visibility: hidden; + -webkit-overflow-scrolling: touch; +} + +@media only screen and (max-width: 767.98px) { + .ui.search.dropdown .menu { + max-height: 8.01428571rem; + } +} + +@media only screen and (min-width: 768px) { + .ui.search.dropdown .menu { + max-height: 10.68571429rem; + } +} + +@media only screen and (min-width: 992px) { + .ui.search.dropdown .menu { + max-height: 16.02857143rem; + } +} + +@media only screen and (min-width: 1920px) { + .ui.search.dropdown .menu { + max-height: 21.37142857rem; + } +} + +.ui.dropdown > .remove.icon { + cursor: pointer; + font-size: 0.85714286em; + margin: -0.78571429em; + padding: 0.91666667em; + right: 3em; + top: 0.78571429em; + position: absolute; + opacity: 0.6; + z-index: 3; +} + +.ui.clearable.dropdown .text, +.ui.clearable.dropdown a:last-of-type { + margin-right: 1.5em; +} + +.ui.dropdown select.noselection ~ .remove.icon, +.ui.dropdown input[value=""] ~ .remove.icon, +.ui.dropdown input:not([value]) ~ .remove.icon, +.ui.dropdown.loading > .remove.icon { + display: none; +} + +.ui.ui.multiple.dropdown { + padding: 0.22619048em 3.2em 0.22619048em 0.35714286em; +} + +.ui.multiple.dropdown .menu { + cursor: auto; +} + +.ui.multiple.dropdown > .label { + display: inline-block; + white-space: normal; + font-size: 1em; + padding: 0.35714286em 0.78571429em; + margin: 0.14285714rem 0.28571429rem 0.14285714rem 0; + box-shadow: 0 0 0 1px var(--color-secondary) inset; +} + +/* Text */ +.ui.multiple.dropdown > .text { + position: static; + padding: 0; + max-width: 100%; + margin: 0.45238095em 0 0.45238095em 0.64285714em; + line-height: 1.21428571em; +} + +.ui.multiple.dropdown > .text.default { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ui.multiple.dropdown > .label ~ input.search { + margin-left: 0.14285714em !important; +} + +.ui.multiple.dropdown > .label ~ .text { + display: none; +} + +.ui.multiple.search.dropdown, +.ui.multiple.search.dropdown > input.search { + cursor: text; +} + +.ui.multiple.search.dropdown > .text { + display: inline-block; + position: absolute; + top: 0; + left: 0; + padding: inherit; + margin: 0.45238095em 0 0.45238095em 0.64285714em; + line-height: 1.21428571em; +} + +.ui.multiple.search.dropdown > .label ~ .text { + display: none; +} + +.ui.multiple.search.dropdown > input.search { + position: static; + padding: 0; + max-width: 100%; + margin: 0.45238095em 0 0.45238095em 0.64285714em; + width: 2.2em; + line-height: 1.21428571em; +} + +.ui.dropdown .menu .active.item { + background: var(--color-active); + font-weight: var(--font-weight-normal); + color: var(--color-text); + box-shadow: none; + z-index: 12; + border-radius: 0; +} + +.ui.dropdown .menu > .item:hover { + background: var(--color-hover); + color: var(--color-text); + z-index: 13; +} + +.ui.dropdown .menu > .item:active { + color: var(--color-text); + background: var(--color-active); +} + +.ui.dropdown:not(.button) > .default.text, +.ui.default.dropdown:not(.button) > .text { + color: var(--color-placeholder-text); +} + +.ui.dropdown:not(.button) > input:focus ~ .default.text, +.ui.default.dropdown:not(.button) > input:focus ~ .text { + color: var(--color-placeholder-text); +} + +.ui.loading.dropdown > i.icon { + height: 1em !important; +} + +.ui.loading.selection.dropdown > i.icon { + padding: 1.5em 1.28571429em !important; +} + +.ui.loading.dropdown > i.icon::before { + position: absolute; + content: ""; + top: 50%; + left: 50%; + margin: -0.64285714em 0 0 -0.64285714em; + width: 1.28571429em; + height: 1.28571429em; + border-radius: 500rem; + border: 0.2em solid var(--color-secondary); +} + +.ui.loading.dropdown > i.icon::after { + position: absolute; + content: ""; + top: 50%; + left: 50%; + box-shadow: 0 0 0 1px transparent; + margin: -0.64285714em 0 0 -0.64285714em; + width: 1.28571429em; + height: 1.28571429em; + animation: loader 0.6s infinite linear; + border: 0.2em solid var(--color-text-light-2); + border-radius: 500rem; +} + +.ui.dropdown .loading.menu { + display: block; + visibility: hidden; + z-index: -1; +} + +.ui.dropdown > .loading.menu { + left: 0 !important; + right: auto !important; +} + +.ui.dropdown > .menu .loading.menu { + left: 100% !important; + right: auto !important; +} + +.ui.dropdown.selected, +.ui.dropdown .menu .selected.item { + color: var(--color-text); + background: var(--color-hover); +} + +.ui.dropdown > .filtered.text { + visibility: hidden; +} + +.ui.dropdown .filtered.item { + display: none !important; +} + +.ui.disabled.dropdown, +.ui.dropdown .menu > .disabled.item { + cursor: default; + pointer-events: none; + opacity: var(--opacity-disabled); +} + +.ui.dropdown > .left.menu { + left: auto !important; + right: 0 !important; +} + +.ui.upward.dropdown > .menu { + top: auto; + bottom: 100%; + box-shadow: 0 6px 18px var(--color-shadow); + border-radius: 0.28571429rem 0.28571429rem 0 0; +} + +.ui.dropdown .upward.menu { + top: auto !important; + bottom: 0 !important; +} + +.ui.upward.selection.dropdown .menu { + border-top-width: 1px !important; + border-bottom-width: 0 !important; + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.upward.selection.dropdown:hover { + box-shadow: 0 0 2px 0 var(--color-shadow); +} + +.ui.active.upward.selection.dropdown { + border-radius: 0 0 0.28571429rem 0.28571429rem !important; +} + +.ui.upward.selection.dropdown.visible { + box-shadow: 0 0 3px 0 var(--color-shadow); + border-radius: 0 0 0.28571429rem 0.28571429rem !important; +} + +.ui.upward.active.selection.dropdown:hover { + box-shadow: 0 0 3px 0 var(--color-shadow); +} + +.ui.upward.active.selection.dropdown:hover .menu { + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.scrolling.dropdown .menu, +.ui.dropdown .scrolling.menu { + overflow-x: hidden; + overflow-y: auto; +} + +.ui.scrolling.dropdown .menu { + overflow-x: hidden; + overflow-y: auto; + backface-visibility: hidden; + -webkit-overflow-scrolling: touch; + min-width: 100% !important; + width: auto !important; +} + +.ui.dropdown .scrolling.menu { + position: static; + overflow-y: auto; + border: none; + box-shadow: none !important; + margin: 0 !important; + min-width: 100% !important; + width: auto !important; + border-top: 1px solid var(--color-secondary); + border-color: var(--color-secondary); + border-radius: 0 0 var(--border-radius) var(--border-radius) !important; +} + +.ui.scrolling.dropdown .menu .item.item.item, +.ui.dropdown .scrolling.menu > .item.item.item { + border-top: none; +} + +.ui.scrolling.dropdown .menu .item:first-child, +.ui.dropdown .scrolling.menu .item:first-child { + border-top: none; +} + +.ui.dropdown > .animating.menu .scrolling.menu, +.ui.dropdown > .visible.menu .scrolling.menu { + display: block; +} + +@media only screen and (max-width: 767.98px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 10.28571429rem; + } +} + +@media only screen and (min-width: 768px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 15.42857143rem; + } +} + +@media only screen and (min-width: 992px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 20.57142857rem; + } +} + +@media only screen and (min-width: 1920px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 20.57142857rem; + } +} + +.ui.fluid.dropdown { + display: block; + width: 100% !important; + min-width: 0; +} + +.ui.fluid.dropdown > .dropdown.icon { + float: right; +} + +.ui.floating.dropdown .menu { + left: 0; + right: auto; + box-shadow: 0 6px 18px var(--color-shadow) !important; + border-radius: 0.28571429rem !important; +} + +.ui.floating.dropdown > .menu { + border-radius: 0.28571429rem !important; +} + +.ui:not(.upward).floating.dropdown > .menu { + margin-top: 0.5em; +} + +.ui.upward.floating.dropdown > .menu { + margin-bottom: 0.5em; +} + +.ui.pointing.dropdown > .menu { + top: 100%; + margin-top: 0.78571429rem; + border-radius: 0.28571429rem; +} + +.ui.pointing.dropdown > .menu:not(.hidden)::after { + display: block; + position: absolute; + pointer-events: none; + content: ""; + visibility: visible; + transform: rotate(45deg); + width: 0.5em; + height: 0.5em; + box-shadow: -1px -1px 0 0 var(--color-secondary); + background: var(--color-menu); + z-index: 2; + top: -0.25em; + left: 50%; + margin: 0 0 0 -0.25em; +} + +.ui.top.right.pointing.dropdown > .menu { + inset: 100% 0 auto auto; + margin: 1em 0 0; +} + +.ui.top.pointing.dropdown > .left.menu::after, +.ui.top.right.pointing.dropdown > .menu::after { + top: -0.25em; + left: auto !important; + right: 1em !important; + margin: 0; + transform: rotate(45deg); +} + +.ui.dropdown, +.ui.dropdown .menu > .item { + font-size: 1rem; +} + +.ui.mini.dropdown, +.ui.mini.dropdown .menu > .item { + font-size: 0.78571429rem; +} + +.ui.tiny.dropdown, +.ui.tiny.dropdown .menu > .item { + font-size: 0.85714286rem; +} + +.ui.small.dropdown, +.ui.small.dropdown .menu > .item { + font-size: 0.92857143rem; +} + +/* This rule must come AFTER .ui.selection.dropdown because both have + specificity (0,3,0) and source order determines the winner. + In the original codebase this was in base.css which loaded after fomantic. */ +.ui.dropdown:not(.button) { + line-height: var(--line-height-default); +} + +/* Icons / Flags / Labels / Image */ +.ui.dropdown > .text > img, +.ui.dropdown > .text > .image, +.ui.dropdown .menu > .item > .image, +.ui.dropdown .menu > .item > img { + margin-left: 0; + float: none; + margin-right: 0.78571429rem; +} + +.ui.dropdown .menu > .item > svg { + margin-right: 0.78rem; +} + +/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */ +.ui.dropdown > .text > .img { + margin-left: 0; + float: none; + margin-right: 0.78571429rem; +} + +.ui.dropdown > .text > img, +.ui.dropdown > .text > .image:not(.icon), +.ui.dropdown .menu > .item > .image:not(.icon), +.ui.dropdown .menu > .item > img { + display: inline-block; + vertical-align: top; + width: auto; + margin-top: -0.25rem; + margin-bottom: -0.25rem; + max-height: 2em; +} + +.ui.dropdown .menu .item { + border-radius: 0; +} + +.ui.dropdown .menu .item:first-of-type { + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.ui.dropdown .menu .item:last-of-type { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +/* Gitea uses SVG images instead of Fomantic builtin "" font icons, so we need to reset the icon styles */ +.ui.ui.dropdown > .icon.icon { + position: initial; + padding: 0; + margin: 0; + height: auto; +} + +.ui.ui.dropdown > .icon.icon:hover { + opacity: 1; +} + +.ui.ui.button.dropdown > .icon.icon, +.ui.ui.selection.dropdown > .icon.icon { + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.ui.ui.dropdown > .dropdown.icon { + right: 0.5em; +} + +.ui.ui.dropdown > .remove.icon { + right: 2em; +} + +.ui.dropdown.mini.button, +.ui.dropdown.tiny.button { + padding-right: 20px; +} + +.ui.dropdown.button { + padding-right: 22px; +} + +.ui.multiple.selection.dropdown { + flex-wrap: wrap; +} + +.ui.ui.dropdown.selection { + min-width: 14em; +} + +.ui.dropdown .ui.label .svg { + vertical-align: middle; +} + +.ui.dropdown > .ui.button { + margin: 0; +} + +/* popover box shadow for menu dropdown */ +.ui.menu .dropdown.item .menu { + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.dropdown .menu.context-user-switch .scrolling.menu { + border-radius: 0 !important; + box-shadow: none !important; + border-bottom: 1px solid var(--color-secondary); + max-width: 80vw; +} + +.scrolling.menu .item.selected { + font-weight: var(--font-weight-semibold) !important; +} + +/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content +the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */ +.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) { + display: flex !important; + align-items: center; + gap: var(--gap-block); + min-width: 0; +} + +.ui.dropdown .menu.flex-items-menu > .item img, +.ui.dropdown .menu.flex-items-menu > .item svg { + margin: 0; +} + +.ui.dropdown.ellipsis-text-items { + /* reset y padding and use the line-height below instead, to avoid the "overflow: hidden" clips the larger image in the "text" element */ + padding-top: 0; + padding-bottom: 0; +} + +.ui.dropdown.ellipsis-text-items > .text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + line-height: 2.71; +} + +.ui.dropdown.ellipsis-text-items .menu > .item { + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} diff --git a/web_src/fomantic/build/components/dropdown.css b/web_src/fomantic/build/components/dropdown.css deleted file mode 100644 index b7b35a2f05..0000000000 --- a/web_src/fomantic/build/components/dropdown.css +++ /dev/null @@ -1,1755 +0,0 @@ -/*! - * # Fomantic-UI - Dropdown - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - - -/******************************* - Dropdown -*******************************/ - -.ui.dropdown { - cursor: pointer; - position: relative; - display: inline-block; - outline: none; - text-align: left; - transition: box-shadow 0.1s ease, width 0.1s ease; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - - -/******************************* - Content -*******************************/ - - -/*-------------- - Menu ----------------*/ - -.ui.dropdown .menu { - cursor: auto; - position: absolute; - display: none; - outline: none; - top: 100%; - min-width: -moz-max-content; - min-width: max-content; - margin: 0; - padding: 0 0; - background: #FFFFFF; - font-size: 1em; - text-shadow: none; - text-align: left; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); - border: 1px solid rgba(34, 36, 38, 0.15); - border-radius: 0.28571429rem; - transition: opacity 0.1s ease; - z-index: 11; - will-change: transform, opacity; -} -.ui.dropdown .menu > * { - white-space: nowrap; -} - -/*-------------- - Hidden Input ----------------*/ - -.ui.dropdown > input:not(.search):first-child, -.ui.dropdown > select { - display: none !important; -} - -/*-------------- - Dropdown Icon ----------------*/ - -.ui.dropdown:not(.labeled) > .dropdown.icon { - position: relative; - width: auto; - font-size: 0.85714286em; - margin: 0 0 0 1em; -} -.ui.dropdown .menu > .item .dropdown.icon { - width: auto; - float: right; - margin: 0em 0 0 1em; -} -.ui.dropdown .menu > .item .dropdown.icon + .text { - margin-right: 1em; -} - -/*-------------- - Text ----------------*/ - -.ui.dropdown > .text { - display: inline-block; - transition: none; -} - -/*-------------- - Menu Item ----------------*/ - -.ui.dropdown .menu > .item { - position: relative; - cursor: pointer; - display: block; - border: none; - height: auto; - min-height: 2.57142857rem; - text-align: left; - border-top: none; - line-height: 1em; - font-size: 1rem; - color: rgba(0, 0, 0, 0.87); - padding: 0.78571429rem 1.14285714rem !important; - text-transform: none; - font-weight: normal; - box-shadow: none; - -webkit-touch-callout: none; -} -.ui.dropdown .menu > .item:first-child { - border-top-width: 0; -} -.ui.dropdown .menu > .item.vertical { - display: flex; - flex-direction: column-reverse; -} - -/*-------------- - Floated Content ----------------*/ - -.ui.dropdown > .text > [class*="right floated"], -.ui.dropdown .menu .item > [class*="right floated"] { - float: right !important; - margin-right: 0 !important; - margin-left: 1em !important; -} -.ui.dropdown > .text > [class*="left floated"], -.ui.dropdown .menu .item > [class*="left floated"] { - float: left !important; - margin-left: 0 !important; - margin-right: 1em !important; -} -.ui.dropdown .menu .item > i.icon.floated, -.ui.dropdown .menu .item > .flag.floated, -.ui.dropdown .menu .item > .image.floated, -.ui.dropdown .menu .item > img.floated { - margin-top: 0em; -} - -/*-------------- - Menu Divider ----------------*/ - -.ui.dropdown .menu > .header { - margin: 1rem 0 0.75rem; - padding: 0 1.14285714rem; - font-weight: 500; - text-transform: uppercase; -} -.ui.dropdown .menu > .header:not(.ui) { - color: rgba(0, 0, 0, 0.85); - font-size: 0.78571429em; -} -.ui.dropdown .menu > .divider { - border-top: 1px solid rgba(34, 36, 38, 0.1); - height: 0; - margin: 0.5em 0; -} -.ui.dropdown .menu > .horizontal.divider { - border-top: none; -} -.ui.dropdown.dropdown .menu > .input { - width: auto; - display: flex; - margin: 1.14285714rem 0.78571429rem; - min-width: 10rem; -} -.ui.dropdown .menu > .header + .input { - margin-top: 0; -} -.ui.dropdown .menu > .input:not(.transparent) input { - padding: 0.5em 1em; -} -.ui.dropdown .menu > .input:not(.transparent) .button, -.ui.dropdown .menu > .input:not(.transparent) i.icon, -.ui.dropdown .menu > .input:not(.transparent) .label { - padding-top: 0.5em; - padding-bottom: 0.5em; -} - -/*----------------- - Item Description --------------------*/ - -.ui.dropdown > .text > .description, -.ui.dropdown .menu > .item > .description { - float: right; - margin: 0 0 0 1em; - color: rgba(0, 0, 0, 0.4); -} -.ui.dropdown .menu > .item.vertical > .description { - margin: 0; -} - -/*----------------- - Item Text --------------------*/ - -.ui.dropdown .menu > .item.vertical > .text { - margin-bottom: 0.25em; -} - -/*----------------- - Message --------------------*/ - -.ui.dropdown .menu > .message { - padding: 0.78571429rem 1.14285714rem; - font-weight: normal; -} -.ui.dropdown .menu > .message:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -/*-------------- - Sub Menu ----------------*/ - -.ui.dropdown .menu .menu { - top: 0; - left: 100%; - right: auto; - margin: 0 -0.5em !important; - border-radius: 0.28571429rem !important; - z-index: 21 !important; -} - -/* Hide Arrow */ -.ui.dropdown .menu .menu:after { - display: none; -} - -/*-------------- - Sub Elements ----------------*/ - - -/* Icons / Flags / Labels / Image */ -.ui.dropdown > .text > i.icon, -.ui.dropdown > .text > .label, -.ui.dropdown > .text > .flag, -.ui.dropdown > .text > img, -.ui.dropdown > .text > .image { - margin-top: 0em; -} -.ui.dropdown .menu > .item > i.icon, -.ui.dropdown .menu > .item > .label, -.ui.dropdown .menu > .item > .flag, -.ui.dropdown .menu > .item > .image, -.ui.dropdown .menu > .item > img { - margin-top: 0em; -} -.ui.dropdown > .text > i.icon, -.ui.dropdown > .text > .label, -.ui.dropdown > .text > .flag, -.ui.dropdown > .text > img, -.ui.dropdown > .text > .image, -.ui.dropdown .menu > .item > i.icon, -.ui.dropdown .menu > .item > .label, -.ui.dropdown .menu > .item > .flag, -.ui.dropdown .menu > .item > .image, -.ui.dropdown .menu > .item > img { - margin-left: 0; - float: none; - margin-right: 0.78571429rem; -} - -/*-------------- - Image ----------------*/ - -.ui.dropdown > .text > img, -.ui.dropdown > .text > .image:not(.icon), -.ui.dropdown .menu > .item > .image:not(.icon), -.ui.dropdown .menu > .item > img { - display: inline-block; - vertical-align: top; - width: auto; - margin-top: -0.5em; - margin-bottom: -0.5em; - max-height: 2em; -} - - -/******************************* - Coupling -*******************************/ - - -/*-------------- - Menu ----------------*/ - - -/* Remove Menu Item Divider */ -.ui.dropdown .ui.menu > .item:before, -.ui.menu .ui.dropdown .menu > .item:before { - display: none; -} - -/* Prevent Menu Item Border */ -.ui.menu .ui.dropdown .menu .active.item { - border-left: none; -} - -/* Automatically float dropdown menu right on last menu item */ -.ui.menu .right.menu .dropdown:last-child > .menu:not(.left), -.ui.menu .right.dropdown.item > .menu:not(.left), -.ui.buttons > .ui.dropdown:last-child > .menu:not(.left) { - left: auto; - right: 0; -} - -/*-------------- - Label - ---------------*/ - - -/* Dropdown Menu */ -.ui.label.dropdown .menu { - min-width: 100%; -} - -/*-------------- - Button - ---------------*/ - - -/* No Margin On Icon Button */ -.ui.dropdown.icon.button > .dropdown.icon { - margin: 0; -} -.ui.button.dropdown .menu { - min-width: 100%; -} - - -/******************************* - Types -*******************************/ - -select.ui.dropdown { - height: 38px; - padding: 0.5em; - border: 1px solid rgba(34, 36, 38, 0.15); - visibility: visible; -} - -/*-------------- - Selection - ---------------*/ - - -/* Displays like a select box */ -.ui.selection.dropdown { - cursor: pointer; - word-wrap: break-word; - line-height: 1em; - white-space: normal; - outline: 0; - transform: rotateZ(0deg); - min-width: 14em; - min-height: 2.71428571em; - background: #FFFFFF; - display: inline-block; - padding: 0.78571429em 3.2em 0.78571429em 1em; - color: rgba(0, 0, 0, 0.87); - box-shadow: none; - border: 1px solid rgba(34, 36, 38, 0.15); - border-radius: 0.28571429rem; - transition: box-shadow 0.1s ease, width 0.1s ease; -} -.ui.selection.dropdown.visible, -.ui.selection.dropdown.active { - z-index: 10; -} -.ui.selection.dropdown > .search.icon, -.ui.selection.dropdown > .delete.icon, -.ui.selection.dropdown > .dropdown.icon { - cursor: pointer; - position: absolute; - width: auto; - height: auto; - line-height: 1.21428571em; - top: 0.78571429em; - right: 1em; - z-index: 3; - margin: -0.78571429em; - padding: 0.91666667em; - opacity: 0.8; - transition: opacity 0.1s ease; -} - -/* Compact */ -.ui.compact.selection.dropdown { - min-width: 0; -} - -/* Selection Menu */ -.ui.selection.dropdown .menu { - overflow-x: hidden; - overflow-y: auto; - backface-visibility: hidden; - -webkit-overflow-scrolling: touch; - border-top-width: 0 !important; - width: auto; - outline: none; - margin: 0 -1px; - min-width: calc(100% + 2px); - width: calc(100% + 2px); - border-radius: 0 0 0.28571429rem 0.28571429rem; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); - transition: opacity 0.1s ease; -} -.ui.selection.dropdown .menu:after, -.ui.selection.dropdown .menu:before { - display: none; -} - -/*-------------- - Message - ---------------*/ - -.ui.selection.dropdown .menu > .message { - padding: 0.78571429rem 1.14285714rem; -} -@media only screen and (max-width: 767.98px) { - .ui.selection.dropdown.short .menu { - max-height: 6.01071429rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 4.00714286rem; - } - .ui.selection.dropdown .menu { - max-height: 8.01428571rem; - } - .ui.selection.dropdown.long .menu { - max-height: 16.02857143rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 24.04285714rem; - } -} -@media only screen and (min-width: 768px) { - .ui.selection.dropdown.short .menu { - max-height: 8.01428571rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 5.34285714rem; - } - .ui.selection.dropdown .menu { - max-height: 10.68571429rem; - } - .ui.selection.dropdown.long .menu { - max-height: 21.37142857rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 32.05714286rem; - } -} -@media only screen and (min-width: 992px) { - .ui.selection.dropdown.short .menu { - max-height: 12.02142857rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 8.01428571rem; - } - .ui.selection.dropdown .menu { - max-height: 16.02857143rem; - } - .ui.selection.dropdown.long .menu { - max-height: 32.05714286rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 48.08571429rem; - } -} -@media only screen and (min-width: 1920px) { - .ui.selection.dropdown.short .menu { - max-height: 16.02857143rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 10.68571429rem; - } - .ui.selection.dropdown .menu { - max-height: 21.37142857rem; - } - .ui.selection.dropdown.long .menu { - max-height: 42.74285714rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 64.11428571rem; - } -} - -/* Menu Item */ -.ui.selection.dropdown .menu > .item { - border-top: 1px solid #FAFAFA; - padding: 0.78571429rem 1.14285714rem !important; - white-space: normal; - word-wrap: normal; -} - -/* User Item */ -.ui.selection.dropdown .menu > .hidden.addition.item { - display: none; -} - -/* Hover */ -.ui.selection.dropdown:hover { - border-color: rgba(34, 36, 38, 0.35); - box-shadow: none; -} - -/* Active */ -.ui.selection.active.dropdown { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} -.ui.selection.active.dropdown .menu { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} - -/* Focus */ -.ui.selection.dropdown:focus { - border-color: #96C8DA; - box-shadow: none; -} -.ui.selection.dropdown:focus .menu { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} - -/* Visible */ -.ui.selection.visible.dropdown > .text:not(.default) { - font-weight: normal; - color: rgba(0, 0, 0, 0.8); -} - -/* Visible Hover */ -.ui.selection.active.dropdown:hover { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} -.ui.selection.active.dropdown:hover .menu { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} - -/* Dropdown Icon */ -.ui.active.selection.dropdown > .dropdown.icon, -.ui.visible.selection.dropdown > .dropdown.icon { - opacity: ''; - z-index: 3; -} - -/* Connecting Border */ -.ui.active.selection.dropdown { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - -/* Empty Connecting Border */ -.ui.active.empty.selection.dropdown { - border-radius: 0.28571429rem !important; - box-shadow: none !important; -} -.ui.active.empty.selection.dropdown .menu { - border: none !important; - box-shadow: none !important; -} - -/* CSS specific to iOS devices or firefox mobile only */ -@supports (-webkit-touch-callout: none) or (-webkit-overflow-scrolling: touch) or (-moz-appearance:none) { - @media (-moz-touch-enabled), (pointer: coarse) { - .ui.dropdown .scrollhint.menu:not(.hidden):before { - animation: scrollhint 2s ease 2; - content: ''; - z-index: 15; - display: block; - position: absolute; - opacity: 0; - right: 0.25em; - top: 0; - height: 100%; - border-right: 0.25em solid; - border-left: 0; - -o-border-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)) 1 100%; - border-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)) 1 100%; - } - .ui.inverted.dropdown .scrollhint.menu:not(.hidden):before { - -o-border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%; - border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%; - } - @keyframes scrollhint { - 0% { - opacity: 1; - top: 100%; - } - 100% { - opacity: 0; - top: 0; - } - } - } -} - -/*-------------- - Searchable - ---------------*/ - - -/* Search Selection */ -.ui.search.dropdown { - min-width: ''; -} - -/* Search Dropdown */ -.ui.search.dropdown > input.search { - background: none transparent !important; - border: none !important; - box-shadow: none !important; - cursor: text; - top: 0; - left: 1px; - width: 100%; - outline: none; - -webkit-tap-highlight-color: rgba(255, 255, 255, 0); - padding: inherit; -} - -/* Text Layering */ -.ui.search.dropdown > input.search { - position: absolute; - z-index: 2; -} -.ui.search.dropdown > .text { - cursor: text; - position: relative; - left: 1px; - z-index: auto; -} - -/* Search Selection */ -.ui.search.selection.dropdown > input.search { - line-height: 1.21428571em; - padding: 0.67857143em 3.2em 0.67857143em 1em; -} - -/* Used to size multi select input to character width */ -.ui.search.selection.dropdown > span.sizer { - line-height: 1.21428571em; - padding: 0.67857143em 3.2em 0.67857143em 1em; - display: none; - white-space: pre; -} - -/* Active/Visible Search */ -.ui.search.dropdown.active > input.search, -.ui.search.dropdown.visible > input.search { - cursor: auto; -} -.ui.search.dropdown.active > .text, -.ui.search.dropdown.visible > .text { - pointer-events: none; -} - -/* Filtered Text */ -.ui.active.search.dropdown input.search:focus + .text i.icon, -.ui.active.search.dropdown input.search:focus + .text .flag { - opacity: var(--opacity-disabled); -} -.ui.active.search.dropdown input.search:focus + .text { - color: rgba(115, 115, 115, 0.87) !important; -} -.ui.search.dropdown.button > span.sizer { - display: none; -} - -/* Search Menu */ -.ui.search.dropdown .menu { - overflow-x: hidden; - overflow-y: auto; - backface-visibility: hidden; - -webkit-overflow-scrolling: touch; -} -@media only screen and (max-width: 767.98px) { - .ui.search.dropdown .menu { - max-height: 8.01428571rem; - } -} -@media only screen and (min-width: 768px) { - .ui.search.dropdown .menu { - max-height: 10.68571429rem; - } -} -@media only screen and (min-width: 992px) { - .ui.search.dropdown .menu { - max-height: 16.02857143rem; - } -} -@media only screen and (min-width: 1920px) { - .ui.search.dropdown .menu { - max-height: 21.37142857rem; - } -} - -/* Clearable Selection */ -.ui.dropdown > .remove.icon { - cursor: pointer; - font-size: 0.85714286em; - margin: -0.78571429em; - padding: 0.91666667em; - right: 3em; - top: 0.78571429em; - position: absolute; - opacity: 0.6; - z-index: 3; -} -.ui.clearable.dropdown .text, -.ui.clearable.dropdown a:last-of-type { - margin-right: 1.5em; -} -.ui.dropdown select.noselection ~ .remove.icon, -.ui.dropdown input[value=''] ~ .remove.icon, -.ui.dropdown input:not([value]) ~ .remove.icon, -.ui.dropdown.loading > .remove.icon { - display: none; -} - -/*-------------- - Multiple - ---------------*/ - - -/* Multiple Selection */ -.ui.ui.multiple.dropdown { - padding: 0.22619048em 3.2em 0.22619048em 0.35714286em; -} -.ui.multiple.dropdown .menu { - cursor: auto; -} - -/* Selection Label */ -.ui.multiple.dropdown > .label { - display: inline-block; - white-space: normal; - font-size: 1em; - padding: 0.35714286em 0.78571429em; - margin: 0.14285714rem 0.28571429rem 0.14285714rem 0; - box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset; -} - -/* Dropdown Icon */ -.ui.multiple.dropdown .dropdown.icon { - margin: ''; - padding: ''; -} - -/* Text */ -.ui.multiple.dropdown > .text { - position: static; - padding: 0; - max-width: 100%; - margin: 0.45238095em 0 0.45238095em 0.64285714em; - line-height: 1.21428571em; -} -.ui.multiple.dropdown > .text.default { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.ui.multiple.dropdown > .label ~ input.search { - margin-left: 0.14285714em !important; -} -.ui.multiple.dropdown > .label ~ .text { - display: none; -} -.ui.multiple.dropdown > .label:not(.image) > img:not(.centered) { - margin-right: 0.78571429rem; -} -.ui.multiple.dropdown > .label:not(.image) > img.ui:not(.avatar) { - margin-bottom: 0.39285714rem; -} -.ui.multiple.dropdown > .image.label img { - margin: -0.35714286em 0.78571429em -0.35714286em -0.78571429em; - height: 1.71428571em; -} - -/*----------------- - Multiple Search - -----------------*/ - - -/* Multiple Search Selection */ -.ui.multiple.search.dropdown, -.ui.multiple.search.dropdown > input.search { - cursor: text; -} - -/* Prompt Text */ -.ui.multiple.search.dropdown > .text { - display: inline-block; - position: absolute; - top: 0; - left: 0; - padding: inherit; - margin: 0.45238095em 0 0.45238095em 0.64285714em; - line-height: 1.21428571em; -} -.ui.multiple.search.dropdown > .label ~ .text { - display: none; -} - -/* Search */ -.ui.multiple.search.dropdown > input.search { - position: static; - padding: 0; - max-width: 100%; - margin: 0.45238095em 0 0.45238095em 0.64285714em; - width: 2.2em; - line-height: 1.21428571em; -} -.ui.multiple.search.dropdown.button { - min-width: 14em; -} - -/*-------------- - Inline - ---------------*/ - -.ui.inline.dropdown { - cursor: pointer; - display: inline-block; - color: inherit; -} -.ui.inline.dropdown .dropdown.icon { - margin: 0 0.21428571em 0 0.21428571em; - vertical-align: baseline; -} -.ui.inline.dropdown > .text { - font-weight: 500; -} -.ui.inline.dropdown .menu { - cursor: auto; - margin-top: 0.21428571em; - border-radius: 0.28571429rem; -} - - -/******************************* - States -*******************************/ - - -/*-------------------- - Active -----------------------*/ - - -/* Menu Item Active */ -.ui.dropdown .menu .active.item { - background: transparent; - font-weight: 500; - color: rgba(0, 0, 0, 0.95); - box-shadow: none; - z-index: 12; -} - -/*-------------------- - Hover -----------------------*/ - - -/* Menu Item Hover */ -.ui.dropdown .menu > .item:hover { - background: rgba(0, 0, 0, 0.05); - color: rgba(0, 0, 0, 0.95); - z-index: 13; -} - -/*-------------------- - Default Text -----------------------*/ - -.ui.dropdown:not(.button) > .default.text, -.ui.default.dropdown:not(.button) > .text { - color: rgba(191, 191, 191, 0.87); -} -.ui.dropdown:not(.button) > input:focus ~ .default.text, -.ui.default.dropdown:not(.button) > input:focus ~ .text { - color: rgba(115, 115, 115, 0.87); -} - -/*-------------------- - Loading - ---------------------*/ - -.ui.loading.dropdown > i.icon { - height: 1em !important; -} -.ui.loading.selection.dropdown > i.icon { - padding: 1.5em 1.28571429em !important; -} -.ui.loading.dropdown > i.icon:before { - position: absolute; - content: ''; - top: 50%; - left: 50%; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - border-radius: 500rem; - border: 0.2em solid rgba(0, 0, 0, 0.1); -} -.ui.loading.dropdown > i.icon:after { - position: absolute; - content: ''; - top: 50%; - left: 50%; - box-shadow: 0 0 0 1px transparent; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - animation: loader 0.6s infinite linear; - border: 0.2em solid #767676; - border-radius: 500rem; -} - -/* Coupling */ -.ui.loading.dropdown.button > i.icon:before, -.ui.loading.dropdown.button > i.icon:after { - display: none; -} -.ui.loading.dropdown > .text { - transition: none; -} - -/* Used To Check Position */ -.ui.dropdown .loading.menu { - display: block; - visibility: hidden; - z-index: -1; -} -.ui.dropdown > .loading.menu { - left: 0 !important; - right: auto !important; -} -.ui.dropdown > .menu .loading.menu { - left: 100% !important; - right: auto !important; -} - -/*-------------------- - Keyboard Select -----------------------*/ - - -/* Selected Item */ -.ui.dropdown.selected, -.ui.dropdown .menu .selected.item { - background: rgba(0, 0, 0, 0.03); - color: rgba(0, 0, 0, 0.95); -} - -/*-------------------- - Search Filtered -----------------------*/ - - -/* Filtered Item */ -.ui.dropdown > .filtered.text { - visibility: hidden; -} -.ui.dropdown .filtered.item { - display: none !important; -} - -/*-------------------- - States - ----------------------*/ - -.ui.dropdown.error, -.ui.dropdown.error > .text, -.ui.dropdown.error > .default.text { - color: #9F3A38; -} -.ui.selection.dropdown.error { - background: #FFF6F6; - border-color: #E0B4B4; -} -.ui.selection.dropdown.error:hover { - border-color: #E0B4B4; -} -.ui.multiple.selection.error.dropdown > .label { - border-color: #E0B4B4; -} -.ui.dropdown.error > .menu, -.ui.dropdown.error > .menu .menu { - border-color: #E0B4B4; -} -.ui.dropdown.error > .menu > .item { - color: #9F3A38; -} - -/* Item Hover */ -.ui.dropdown.error > .menu > .item:hover { - background-color: #FBE7E7; -} - -/* Item Active */ -.ui.dropdown.error > .menu .active.item { - background-color: #FDCFCF; -} -.ui.dropdown.info, -.ui.dropdown.info > .text, -.ui.dropdown.info > .default.text { - color: #276F86; -} -.ui.selection.dropdown.info { - background: #F8FFFF; - border-color: #A9D5DE; -} -.ui.selection.dropdown.info:hover { - border-color: #A9D5DE; -} -.ui.multiple.selection.info.dropdown > .label { - border-color: #A9D5DE; -} -.ui.dropdown.info > .menu, -.ui.dropdown.info > .menu .menu { - border-color: #A9D5DE; -} -.ui.dropdown.info > .menu > .item { - color: #276F86; -} - -/* Item Hover */ -.ui.dropdown.info > .menu > .item:hover { - background-color: #e9f2fb; -} - -/* Item Active */ -.ui.dropdown.info > .menu .active.item { - background-color: #cef1fd; -} -.ui.dropdown.success, -.ui.dropdown.success > .text, -.ui.dropdown.success > .default.text { - color: #2C662D; -} -.ui.selection.dropdown.success { - background: #FCFFF5; - border-color: #A3C293; -} -.ui.selection.dropdown.success:hover { - border-color: #A3C293; -} -.ui.multiple.selection.success.dropdown > .label { - border-color: #A3C293; -} -.ui.dropdown.success > .menu, -.ui.dropdown.success > .menu .menu { - border-color: #A3C293; -} -.ui.dropdown.success > .menu > .item { - color: #2C662D; -} - -/* Item Hover */ -.ui.dropdown.success > .menu > .item:hover { - background-color: #e9fbe9; -} - -/* Item Active */ -.ui.dropdown.success > .menu .active.item { - background-color: #dafdce; -} -.ui.dropdown.warning, -.ui.dropdown.warning > .text, -.ui.dropdown.warning > .default.text { - color: #573A08; -} -.ui.selection.dropdown.warning { - background: #FFFAF3; - border-color: #C9BA9B; -} -.ui.selection.dropdown.warning:hover { - border-color: #C9BA9B; -} -.ui.multiple.selection.warning.dropdown > .label { - border-color: #C9BA9B; -} -.ui.dropdown.warning > .menu, -.ui.dropdown.warning > .menu .menu { - border-color: #C9BA9B; -} -.ui.dropdown.warning > .menu > .item { - color: #573A08; -} - -/* Item Hover */ -.ui.dropdown.warning > .menu > .item:hover { - background-color: #fbfbe9; -} - -/* Item Active */ -.ui.dropdown.warning > .menu .active.item { - background-color: #fdfdce; -} - -/*-------------------- - Clear -----------------------*/ - -.ui.dropdown > .clear.dropdown.icon { - opacity: 0.8; - transition: opacity 0.1s ease; -} -.ui.dropdown > .clear.dropdown.icon:hover { - opacity: 1; -} - -/*-------------------- - Disabled - ----------------------*/ - - -/* Disabled */ -.ui.disabled.dropdown, -.ui.dropdown .menu > .disabled.item { - cursor: default; - pointer-events: none; - opacity: var(--opacity-disabled); -} - - -/******************************* - Variations -*******************************/ - - -/*-------------- - Direction ----------------*/ - - -/* Flyout Direction */ -.ui.dropdown .menu { - left: 0; -} - -/* Default Side (Right) */ -.ui.dropdown .right.menu > .menu, -.ui.dropdown .menu .right.menu { - left: 100% !important; - right: auto !important; - border-radius: 0.28571429rem !important; -} - -/* Leftward Opening Menu */ -.ui.dropdown > .left.menu { - left: auto !important; - right: 0 !important; -} -.ui.dropdown > .left.menu .menu, -.ui.dropdown .menu .left.menu { - left: auto; - right: 100%; - margin: 0 -0.5em 0 0 !important; - border-radius: 0.28571429rem !important; -} -.ui.dropdown .item .left.dropdown.icon, -.ui.dropdown .left.menu .item .dropdown.icon { - width: auto; - float: left; - margin: 0em 0 0 0; -} -.ui.dropdown .item .left.dropdown.icon, -.ui.dropdown .left.menu .item .dropdown.icon { - width: auto; - float: left; - margin: 0em 0 0 0; -} -.ui.dropdown .item .left.dropdown.icon + .text, -.ui.dropdown .left.menu .item .dropdown.icon + .text { - margin-left: 1em; - margin-right: 0; -} - -/*-------------- - Upward - ---------------*/ - - -/* Upward Main Menu */ -.ui.upward.dropdown > .menu { - top: auto; - bottom: 100%; - box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08); - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -/* Upward Sub Menu */ -.ui.dropdown .upward.menu { - top: auto !important; - bottom: 0 !important; -} - -/* Active Upward */ -.ui.simple.upward.active.dropdown, -.ui.simple.upward.dropdown:hover { - border-radius: 0.28571429rem 0.28571429rem 0 0 !important; -} -.ui.upward.dropdown.button:not(.pointing):not(.floating).active { - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -/* Selection */ -.ui.upward.selection.dropdown .menu { - border-top-width: 1px !important; - border-bottom-width: 0 !important; - box-shadow: 0 -2px 3px 0 rgba(0, 0, 0, 0.08); -} -.ui.upward.selection.dropdown:hover { - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05); -} - -/* Active Upward */ -.ui.active.upward.selection.dropdown { - border-radius: 0 0 0.28571429rem 0.28571429rem !important; -} - -/* Visible Upward */ -.ui.upward.selection.dropdown.visible { - box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08); - border-radius: 0 0 0.28571429rem 0.28571429rem !important; -} - -/* Visible Hover Upward */ -.ui.upward.active.selection.dropdown:hover { - box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.05); -} -.ui.upward.active.selection.dropdown:hover .menu { - box-shadow: 0 -2px 3px 0 rgba(0, 0, 0, 0.08); -} - -/*-------------- - Scrolling - ---------------*/ - - -/* Selection Menu */ -.ui.scrolling.dropdown .menu, -.ui.dropdown .scrolling.menu { - overflow-x: hidden; - overflow-y: auto; -} -.ui.scrolling.dropdown .menu { - overflow-x: hidden; - overflow-y: auto; - backface-visibility: hidden; - -webkit-overflow-scrolling: touch; - min-width: 100% !important; - width: auto !important; -} -.ui.dropdown .scrolling.menu { - position: static; - overflow-y: auto; - border: none; - box-shadow: none !important; - border-radius: 0 !important; - margin: 0 !important; - min-width: 100% !important; - width: auto !important; - border-top: 1px solid rgba(34, 36, 38, 0.15); -} -.ui.scrolling.dropdown .menu .item.item.item, -.ui.dropdown .scrolling.menu > .item.item.item { - border-top: none; -} -.ui.scrolling.dropdown .menu .item:first-child, -.ui.dropdown .scrolling.menu .item:first-child { - border-top: none; -} -.ui.dropdown > .animating.menu .scrolling.menu, -.ui.dropdown > .visible.menu .scrolling.menu { - display: block; -} - -/* Scrollbar in IE */ -@media all and (-ms-high-contrast: none) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - min-width: calc(100% - 17px); - } -} -@media only screen and (max-width: 767.98px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 10.28571429rem; - } -} -@media only screen and (min-width: 768px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 15.42857143rem; - } -} -@media only screen and (min-width: 992px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 20.57142857rem; - } -} -@media only screen and (min-width: 1920px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 20.57142857rem; - } -} - -/*-------------- - Columnar ----------------*/ - -.ui.column.dropdown > .menu { - flex-wrap: wrap; -} -.ui.dropdown[class*="two column"] > .menu > .item { - width: 50%; -} -.ui.dropdown[class*="three column"] > .menu > .item { - width: 33%; -} -.ui.dropdown[class*="four column"] > .menu > .item { - width: 25%; -} -.ui.dropdown[class*="five column"] > .menu > .item { - width: 20%; -} - -/*-------------- - Simple - ---------------*/ - - -/* Displays without javascript */ -.ui.simple.dropdown .menu:before, -.ui.simple.dropdown .menu:after { - display: none; -} -.ui.simple.dropdown .menu { - position: absolute; - -/* IE hack to make dropdown icons appear inline */ - display: -ms-inline-flexbox !important; - display: block; - overflow: hidden; - top: -9999px; - opacity: 0; - width: 0; - height: 0; - transition: opacity 0.1s ease; - margin-top: 0 !important; -} -.ui.simple.active.dropdown, -.ui.simple.dropdown:hover { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} -.ui.simple.active.dropdown > .menu, -.ui.simple.dropdown:hover > .menu { - overflow: visible; - width: auto; - height: auto; - top: 100%; - opacity: 1; -} -.ui.simple.dropdown > .menu > .item:active > .menu, -.ui.simple.dropdown .menu .item:hover > .menu { - overflow: visible; - width: auto; - height: auto; - top: 0 !important; - left: 100%; - opacity: 1; -} -.ui.simple.dropdown > .menu > .item:active > .left.menu, -.ui.simple.dropdown .menu .item:hover > .left.menu, -.right.menu .ui.simple.dropdown > .menu > .item:active > .menu:not(.right), -.right.menu .ui.simple.dropdown > .menu .item:hover > .menu:not(.right) { - left: auto; - right: 100%; -} -.ui.simple.disabled.dropdown:hover .menu { - display: none; - height: 0; - width: 0; - overflow: hidden; -} - -/* Visible */ -.ui.simple.visible.dropdown > .menu { - display: block; -} - -/* Scrolling */ -.ui.simple.scrolling.active.dropdown > .menu, -.ui.simple.scrolling.dropdown:hover > .menu { - overflow-x: hidden; - overflow-y: auto; -} - -/*-------------- - Fluid - ---------------*/ - -.ui.fluid.dropdown { - display: block; - width: 100% !important; - min-width: 0; -} -.ui.fluid.dropdown > .dropdown.icon { - float: right; -} - -/*-------------- - Floating - ---------------*/ - -.ui.floating.dropdown .menu { - left: 0; - right: auto; - box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15) !important; - border-radius: 0.28571429rem !important; -} -.ui.floating.dropdown > .menu { - border-radius: 0.28571429rem !important; -} -.ui:not(.upward).floating.dropdown > .menu { - margin-top: 0.5em; -} -.ui.upward.floating.dropdown > .menu { - margin-bottom: 0.5em; -} - -/*-------------- - Pointing - ---------------*/ - -.ui.pointing.dropdown > .menu { - top: 100%; - margin-top: 0.78571429rem; - border-radius: 0.28571429rem; -} -.ui.pointing.dropdown > .menu:not(.hidden):after { - display: block; - position: absolute; - pointer-events: none; - content: ''; - visibility: visible; - transform: rotate(45deg); - width: 0.5em; - height: 0.5em; - box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15); - background: #FFFFFF; - z-index: 2; -} -.ui.pointing.dropdown > .menu:not(.hidden):after { - top: -0.25em; - left: 50%; - margin: 0 0 0 -0.25em; -} - -/* Top Left Pointing */ -.ui.top.left.pointing.dropdown > .menu { - top: 100%; - bottom: auto; - left: 0; - right: auto; - margin: 1em 0 0; -} -.ui.top.left.pointing.dropdown > .menu { - top: 100%; - bottom: auto; - left: 0; - right: auto; - margin: 1em 0 0; -} -.ui.top.left.pointing.dropdown > .menu:after { - top: -0.25em; - left: 1em; - right: auto; - margin: 0; - transform: rotate(45deg); -} - -/* Top Right Pointing */ -.ui.top.right.pointing.dropdown > .menu { - top: 100%; - bottom: auto; - right: 0; - left: auto; - margin: 1em 0 0; -} -.ui.top.pointing.dropdown > .left.menu:after, -.ui.top.right.pointing.dropdown > .menu:after { - top: -0.25em; - left: auto !important; - right: 1em !important; - margin: 0; - transform: rotate(45deg); -} - -/* Left Pointing */ -.ui.left.pointing.dropdown > .menu { - top: 0; - left: 100%; - right: auto; - margin: 0 0 0 1em; -} -.ui.left.pointing.dropdown > .menu:after { - top: 1em; - left: -0.25em; - margin: 0 0 0 0; - transform: rotate(-45deg); -} -.ui.left:not(.top):not(.bottom).pointing.dropdown > .left.menu { - left: auto !important; - right: 100% !important; - margin: 0 1em 0 0; -} -.ui.left:not(.top):not(.bottom).pointing.dropdown > .left.menu:after { - top: 1em; - left: auto; - right: -0.25em; - margin: 0 0 0 0; - transform: rotate(135deg); -} - -/* Right Pointing */ -.ui.right.pointing.dropdown > .menu { - top: 0; - left: auto; - right: 100%; - margin: 0 1em 0 0; -} -.ui.right.pointing.dropdown > .menu:after { - top: 1em; - left: auto; - right: -0.25em; - margin: 0 0 0 0; - transform: rotate(135deg); -} - -/* Bottom Pointing */ -.ui.bottom.pointing.dropdown > .menu { - top: auto; - bottom: 100%; - left: 0; - right: auto; - margin: 0 0 1em; -} -.ui.bottom.pointing.dropdown > .menu:after { - top: auto; - bottom: -0.25em; - right: auto; - margin: 0; - transform: rotate(-135deg); -} - -/* Reverse Sub-Menu Direction */ -.ui.bottom.pointing.dropdown > .menu .menu { - top: auto !important; - bottom: 0 !important; -} - -/* Bottom Left */ -.ui.bottom.left.pointing.dropdown > .menu { - left: 0; - right: auto; -} -.ui.bottom.left.pointing.dropdown > .menu:after { - left: 1em; - right: auto; -} - -/* Bottom Right */ -.ui.bottom.right.pointing.dropdown > .menu { - right: 0; - left: auto; -} -.ui.bottom.right.pointing.dropdown > .menu:after { - left: auto; - right: 1em; -} - -/* Upward pointing */ -.ui.pointing.upward.dropdown .menu, -.ui.top.pointing.upward.dropdown .menu { - top: auto !important; - bottom: 100% !important; - margin: 0 0 0.78571429rem; - border-radius: 0.28571429rem; -} -.ui.pointing.upward.dropdown .menu:after, -.ui.top.pointing.upward.dropdown .menu:after { - top: 100% !important; - bottom: auto !important; - box-shadow: 1px 1px 0 0 rgba(34, 36, 38, 0.15); - margin: -0.25em 0 0; -} - -/* Right Pointing Upward */ -.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu { - top: auto !important; - bottom: 0 !important; - margin: 0 1em 0 0; -} -.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after { - top: auto !important; - bottom: 0 !important; - margin: 0 0 1em 0; - box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15); -} - -/* Left Pointing Upward */ -.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu { - top: auto !important; - bottom: 0 !important; - margin: 0 0 0 1em; -} -.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after { - top: auto !important; - bottom: 0 !important; - margin: 0 0 1em 0; - box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15); -} - -/*-------------------- - Sizes ----------------------*/ - -.ui.dropdown, -.ui.dropdown .menu > .item { - font-size: 1rem; -} -.ui.mini.dropdown, -.ui.mini.dropdown .menu > .item { - font-size: 0.78571429rem; -} -.ui.tiny.dropdown, -.ui.tiny.dropdown .menu > .item { - font-size: 0.85714286rem; -} -.ui.small.dropdown, -.ui.small.dropdown .menu > .item { - font-size: 0.92857143rem; -} -.ui.large.dropdown, -.ui.large.dropdown .menu > .item { - font-size: 1.14285714rem; -} -.ui.big.dropdown, -.ui.big.dropdown .menu > .item { - font-size: 1.28571429rem; -} -.ui.huge.dropdown, -.ui.huge.dropdown .menu > .item { - font-size: 1.42857143rem; -} -.ui.massive.dropdown, -.ui.massive.dropdown .menu > .item { - font-size: 1.71428571rem; -} - - -/******************************* - Theme Overrides -*******************************/ - - -/* Dropdown Carets */ -@font-face { - font-family: 'Dropdown'; - src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggjB5AAAAC8AAAAYGNtYXAPfuIIAAABHAAAAExnYXNwAAAAEAAAAWgAAAAIZ2x5Zjo82LgAAAFwAAABVGhlYWQAQ88bAAACxAAAADZoaGVhAwcB6QAAAvwAAAAkaG10eAS4ABIAAAMgAAAAIGxvY2EBNgDeAAADQAAAABJtYXhwAAoAFgAAA1QAAAAgbmFtZVcZpu4AAAN0AAABRXBvc3QAAwAAAAAEvAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADw2gHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIPDa//3//wAAAAAAIPDX//3//wAB/+MPLQADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAIABJQElABMAABM0NzY3BTYXFhUUDwEGJwYvASY1AAUGBwEACAUGBoAFCAcGgAUBEgcGBQEBAQcECQYHfwYBAQZ/BwYAAQAAAG4BJQESABMAADc0PwE2MzIfARYVFAcGIyEiJyY1AAWABgcIBYAGBgUI/wAHBgWABwaABQWABgcHBgUFBgcAAAABABIASQC3AW4AEwAANzQ/ATYXNhcWHQEUBwYnBi8BJjUSBoAFCAcFBgYFBwgFgAbbBwZ/BwEBBwQJ/wgEBwEBB38GBgAAAAABAAAASQClAW4AEwAANxE0NzYzMh8BFhUUDwEGIyInJjUABQYHCAWABgaABQgHBgVbAQAIBQYGgAUIBwWABgYFBwAAAAEAAAABAADZuaKOXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAAAAACgAUAB4AQgBkAIgAqgAAAAEAAAAIABQAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAOAAAAAQAAAAAAAgAOAEcAAQAAAAAAAwAOACQAAQAAAAAABAAOAFUAAQAAAAAABQAWAA4AAQAAAAAABgAHADIAAQAAAAAACgA0AGMAAwABBAkAAQAOAAAAAwABBAkAAgAOAEcAAwABBAkAAwAOACQAAwABBAkABAAOAFUAAwABBAkABQAWAA4AAwABBAkABgAOADkAAwABBAkACgA0AGMAaQBjAG8AbQBvAG8AbgBWAGUAcgBzAGkAbwBuACAAMQAuADAAaQBjAG8AbQBvAG8Abmljb21vb24AaQBjAG8AbQBvAG8AbgBSAGUAZwB1AGwAYQByAGkAYwBvAG0AbwBvAG4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('truetype'), url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAVwAAoAAAAABSgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAdkAAAHZLDXE/09TLzIAAALQAAAAYAAAAGAIIweQY21hcAAAAzAAAABMAAAATA9+4ghnYXNwAAADfAAAAAgAAAAIAAAAEGhlYWQAAAOEAAAANgAAADYAQ88baGhlYQAAA7wAAAAkAAAAJAMHAelobXR4AAAD4AAAACAAAAAgBLgAEm1heHAAAAQAAAAABgAAAAYACFAAbmFtZQAABAgAAAFFAAABRVcZpu5wb3N0AAAFUAAAACAAAAAgAAMAAAEABAQAAQEBCGljb21vb24AAQIAAQA6+BwC+BsD+BgEHgoAGVP/i4seCgAZU/+LiwwHi2v4lPh0BR0AAACIDx0AAACNER0AAAAJHQAAAdASAAkBAQgPERMWGyAlKmljb21vb25pY29tb29udTB1MXUyMHVGMEQ3dUYwRDh1RjBEOXVGMERBAAACAYkABgAIAgABAAQABwAKAA0AVgCfAOgBL/yUDvyUDvyUDvuUDvtvi/emFYuQjZCOjo+Pj42Qiwj3lIsFkIuQiY6Hj4iNhouGi4aJh4eHCPsU+xQFiIiGiYaLhouHjYeOCPsU9xQFiI+Jj4uQCA77b4v3FBWLkI2Pjo8I9xT3FAWPjo+NkIuQi5CJjogI9xT7FAWPh42Hi4aLhomHh4eIiIaJhosI+5SLBYaLh42HjoiPiY+LkAgO+92d928Vi5CNkI+OCPcU9xQFjo+QjZCLkIuPiY6Hj4iNhouGCIv7lAWLhomHh4iIh4eJhouGi4aNiI8I+xT3FAWHjomPi5AIDvvdi+YVi/eUBYuQjZCOjo+Pj42Qi5CLkImOhwj3FPsUBY+IjYaLhouGiYeHiAj7FPsUBYiHhomGi4aLh42Hj4iOiY+LkAgO+JQU+JQViwwKAAAAAAMCAAGQAAUAAAFMAWYAAABHAUwBZgAAAPUAGQCEAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA8NoB4P/g/+AB4AAgAAAAAQAAAAAAAAAAAAAAIAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABAA4AAAACgAIAAIAAgABACDw2v/9//8AAAAAACDw1//9//8AAf/jDy0AAwABAAAAAAAAAAAAAAABAAH//wAPAAEAAAABAAA5emozXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAUAAACAAAAAAADgCuAAEAAAAAAAEADgAAAAEAAAAAAAIADgBHAAEAAAAAAAMADgAkAAEAAAAAAAQADgBVAAEAAAAAAAUAFgAOAAEAAAAAAAYABwAyAAEAAAAAAAoANABjAAMAAQQJAAEADgAAAAMAAQQJAAIADgBHAAMAAQQJAAMADgAkAAMAAQQJAAQADgBVAAMAAQQJAAUAFgAOAAMAAQQJAAYADgA5AAMAAQQJAAoANABjAGkAYwBvAG0AbwBvAG4AVgBlAHIAcwBpAG8AbgAgADEALgAwAGkAYwBvAG0AbwBvAG5pY29tb29uAGkAYwBvAG0AbwBvAG4AUgBlAGcAdQBsAGEAcgBpAGMAbwBtAG8AbwBuAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('woff'); - font-weight: normal; - font-style: normal; -} -.ui.dropdown > .dropdown.icon { - font-family: 'Dropdown'; - line-height: 1; - height: 1em; - width: 1.23em; - backface-visibility: hidden; - font-weight: normal; - font-style: normal; - text-align: center; -} -.ui.dropdown > .dropdown.icon { - width: auto; -} -.ui.dropdown > .dropdown.icon:before { - content: '\f0d7'; -} - -/* Sub Menu */ -.ui.dropdown .menu .item .dropdown.icon:before { - content: '\f0da' /*rtl:'\f0d9'*/; -} -.ui.dropdown .item .left.dropdown.icon:before, -.ui.dropdown .left.menu .item .dropdown.icon:before { - content: "\f0d9" /*rtl:"\f0da"*/; -} - -/* Vertical Menu Dropdown */ -.ui.vertical.menu .dropdown.item > .dropdown.icon:before { - content: "\f0da" /*rtl:"\f0d9"*/; -} -/* Icons for Reference -.dropdown.down.icon { - content: "\f0d7"; -} -.dropdown.up.icon { - content: "\f0d8"; -} -.dropdown.left.icon { - content: "\f0d9"; -} -.dropdown.icon.icon { - content: "\f0da"; -} -*/ - - -/******************************* - User Overrides -*******************************/ - diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index 47d815490d..b8f066db74 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -4158,7 +4158,7 @@ $.fn.dropdown.settings.templates = { html = '', escape = $.fn.dropdown.settings.templates.escape ; - html += ''; + html += ''; if(placeholder) { html += '
    ' + escape(placeholder,preserveHTML) + '
    '; } diff --git a/web_src/fomantic/build/fomantic.css b/web_src/fomantic/build/fomantic.css index e3dd4dcfe2..9b5f654a8e 100644 --- a/web_src/fomantic/build/fomantic.css +++ b/web_src/fomantic/build/fomantic.css @@ -1,3 +1,2 @@ -@import "./components/dropdown.css"; @import "./components/modal.css"; @import "./components/search.css"; diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json index a70bfdd16f..e135528f8f 100644 --- a/web_src/fomantic/semantic.json +++ b/web_src/fomantic/semantic.json @@ -22,7 +22,6 @@ "admin": false, "components": [ "api", - "dropdown", "modal", "search", "tab" From b24780b3a318dcb4abc4634761d8356f4f0d6076 Mon Sep 17 00:00:00 2001 From: yshyuk <43194469+yshyuk@users.noreply.github.com> Date: Sat, 28 Feb 2026 02:25:23 +0900 Subject: [PATCH 023/207] Fix typos and grammar in English locale (#36751) Fix several English locale issues as suggested in #35015: - Rename `enterred` to `entered` in locale keys (`form.enterred_invalid_*`) and update all Go source references accordingly - Fix subject-verb agreement in `oauth2_applications_desc` and `oauth2_application_create_description` - Improve awkward phrasing in `startpage.license_desc` Only `locale_en-US.json` is modified; other locales are managed by Crowdin. Ref #35015 --------- Signed-off-by: yshyuk Co-authored-by: Claude Opus 4.6 --- options/locale/locale_en-US.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index a3dc09bb21..8f7a050b16 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -225,7 +225,7 @@ "startpage.lightweight": "Lightweight", "startpage.lightweight_desc": "Gitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!", "startpage.license": "Open Source", - "startpage.license_desc": "Go get %[2]s! Join us by contributing to make this project even better. Don't be shy to be a contributor!", + "startpage.license_desc": "Go get %[2]s! Join us by contributing to make this project even better. Don't hesitate to contribute!", "install.install": "Installation", "install.installing_desc": "Installing now, please wait…", "install.title": "Initial Configuration", @@ -866,7 +866,7 @@ "settings.permissions_list": "Permissions:", "settings.manage_oauth2_applications": "Manage OAuth2 Applications", "settings.edit_oauth2_application": "Edit OAuth2 Application", - "settings.oauth2_applications_desc": "OAuth2 applications enables your third-party application to securely authenticate users at this Gitea instance.", + "settings.oauth2_applications_desc": "OAuth2 applications enable your third-party application to securely authenticate users at this Gitea instance.", "settings.remove_oauth2_application": "Remove OAuth2 Application", "settings.remove_oauth2_application_desc": "Removing an OAuth2 application will revoke access to all signed access tokens. Continue?", "settings.remove_oauth2_application_success": "The application has been deleted.", @@ -885,7 +885,7 @@ "settings.oauth2_regenerate_secret_hint": "Lost your secret?", "settings.oauth2_client_secret_hint": "The secret will not be shown again after you leave or refresh this page. Please ensure that you have saved it.", "settings.oauth2_application_edit": "Edit", - "settings.oauth2_application_create_description": "OAuth2 applications gives your third-party application access to user accounts on this instance.", + "settings.oauth2_application_create_description": "OAuth2 applications give your third-party application access to user accounts on this instance.", "settings.oauth2_application_remove_description": "Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?", "settings.oauth2_application_locked": "Gitea pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected behavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.", "settings.authorized_oauth2_applications": "Authorized OAuth2 Applications", From 2e00b2f0bb7222998c04cb5b85dded8a198de583 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Feb 2026 23:23:21 +0100 Subject: [PATCH 024/207] Fix `no-content` message not rendering after comment edit (#36733) When non-empty comment content edited is deleted, it would render a empty comment body: image Fix it so it renders the same placeholder HTML that the server sends for empty content before edits: image --- routers/web/repo/issue.go | 10 +++++++++- routers/web/repo/issue_comment.go | 8 +------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index eaec3b5789..a295a3c903 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -21,6 +21,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" @@ -369,7 +370,7 @@ func UpdateIssueContent(ctx *context.Context) { } ctx.JSON(http.StatusOK, map[string]any{ - "content": content, + "content": commentContentHTML(ctx, content), "contentVersion": issue.ContentVersion, "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content), }) @@ -629,6 +630,13 @@ func updateAttachments(ctx *context.Context, item any, files []string) error { return err } +func commentContentHTML(ctx *context.Context, content template.HTML) template.HTML { + if strings.TrimSpace(string(content)) == "" { + return htmlutil.HTMLFormat(`%s`, ctx.Tr("repo.issues.no_content")) + } + return content +} + func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML { attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{ "ctxData": ctx.Data, diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index a3cb88e76a..7f8cc23a3f 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -9,7 +9,6 @@ import ( "html/template" "net/http" "strconv" - "strings" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -17,7 +16,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" repo_module "code.gitea.io/gitea/modules/repository" @@ -291,12 +289,8 @@ func UpdateCommentContent(ctx *context.Context) { } } - if strings.TrimSpace(string(renderedContent)) == "" { - renderedContent = htmlutil.HTMLFormat(`%s`, ctx.Tr("repo.issues.no_content")) - } - ctx.JSON(http.StatusOK, map[string]any{ - "content": renderedContent, + "content": commentContentHTML(ctx, renderedContent), "contentVersion": comment.ContentVersion, "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), }) From 3b250ba04e126f70ac6fa388ae90d508996ed21d Mon Sep 17 00:00:00 2001 From: xiaox <827812965@qq.com> Date: Sat, 28 Feb 2026 21:03:25 +0800 Subject: [PATCH 025/207] refactor: replace legacy tw-flex utility classes with flex-text-block/inline (#36778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replace combinations of `tw-flex tw-items-center` (with optional `tw-gap-*`) with semantic `flex-text-block` or `flex-text-inline` classes across 15 template files. This follows the refactoring direction outlined in #35015 ("Refactor legacy `tw-flex tw-items-center tw-gap-xx` to `flex-text-block` or `flex-text-inline`"). ## Changes ### Replacement rules applied: - `tw-flex tw-items-center tw-gap-2` → `flex-text-block` (both have `gap: 0.5rem`) - `tw-flex tw-items-center tw-gap-1` → `flex-text-inline` (both have `gap: 0.25rem`) - `tw-flex tw-items-center` (no explicit gap) → `flex-text-block` where the element is block-level and children benefit from the default gap - `tw-flex tw-items-center` (inline context, e.g. ``, ``) → `flex-text-inline` ### Files modified (15): - `templates/admin/config.tmpl` — config page dt elements - `templates/admin/repo/unadopted.tmpl` — unadopted repo list items - `templates/base/head_navbar.tmpl` — active stopwatch popup - `templates/org/header.tmpl` — org header action buttons - `templates/org/home.tmpl` — member/team count links - `templates/org/settings/labels.tmpl` — labels page header - `templates/repo/branch/list.tmpl` — branch list header - `templates/repo/commits_table.tmpl` — commits table header - `templates/repo/diff/box.tmpl` — diff detail box - `templates/repo/diff/new_review.tmpl` — review form header - `templates/repo/issue/card.tmpl` — issue card unpin button - `templates/repo/issue/view_content/attachments.tmpl` — attachment file size - `templates/repo/migrate/migrate.tmpl` — migration service cards - `templates/shared/user/org_profile_avatar.tmpl` — org profile header - `templates/webhook/new.tmpl` — webhook type dropdown text ### What was NOT changed: - Elements with `tw-justify-between` or `tw-justify-center` (these need additional classes) - Elements whose children use explicit margins (`tw-mr-*`, `tw-ml-*`) that would conflict with the gap from flex-text classes - Fomantic UI form elements with special layout requirements ## Notes - This PR was created with AI assistance (Claude). All changes were reviewed individually to ensure semantic correctness and zero unintended visual changes. - No functional changes — purely CSS class refactoring. Closes: part of #35015 Signed-off-by: xiaox315 Co-authored-by: xiaox315 --- templates/admin/config.tmpl | 4 ++-- templates/admin/repo/unadopted.tmpl | 2 +- templates/base/head_navbar.tmpl | 4 ++-- templates/org/header.tmpl | 2 +- templates/org/home.tmpl | 4 ++-- templates/org/settings/labels.tmpl | 2 +- templates/repo/branch/list.tmpl | 2 +- templates/repo/commits_table.tmpl | 2 +- templates/repo/diff/box.tmpl | 2 +- templates/repo/diff/new_review.tmpl | 2 +- templates/repo/issue/card.tmpl | 2 +- templates/repo/issue/view_content/attachments.tmpl | 2 +- templates/repo/migrate/migrate.tmpl | 2 +- templates/shared/user/org_profile_avatar.tmpl | 2 +- templates/webhook/new.tmpl | 2 +- 15 files changed, 18 insertions(+), 18 deletions(-) diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index a61dec9620..6dc6e0d5ea 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -223,7 +223,7 @@
    {{ctx.Locale.Tr "admin.config.mailer_user"}}
    {{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}
    -
    {{ctx.Locale.Tr "admin.config.send_test_mail"}}
    +
    {{ctx.Locale.Tr "admin.config.send_test_mail"}}
    @@ -254,7 +254,7 @@
    {{.CacheItemTTL}}
    {{end}}
    -
    {{ctx.Locale.Tr "admin.config.cache_test"}}
    +
    {{ctx.Locale.Tr "admin.config.cache_test"}}
    diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index 54b76c08bc..e66add6ce8 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -20,7 +20,7 @@ {{if .Dirs}}
    {{range $dirI, $dir := .Dirs}} -
    +
    {{svg "octicon-file-directory-fill"}} {{$dir}}
    diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 28fcee023f..43cbcbdc0c 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -151,8 +151,8 @@ {{$activeStopwatch := and .PageGlobalData (call .PageGlobalData.GetActiveStopwatch)}} {{if $activeStopwatch}}
    -
    - +
    + {{svg "octicon-issue-opened" 16}} {{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}} diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 040164ba86..31f43449ab 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -7,7 +7,7 @@ {{if .Org.Visibility.IsLimited}}{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}{{end}} {{if .Org.Visibility.IsPrivate}}{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}{{end}} - + {{if .EnableFeed}} {{svg "octicon-rss" 24}} diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index ea294980e0..ab088ce7b2 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -49,7 +49,7 @@ {{if .NumMembers}}

    {{ctx.Locale.Tr "org.members"}} - {{.NumMembers}} {{svg "octicon-chevron-right"}} + {{.NumMembers}} {{svg "octicon-chevron-right"}}

    {{$isMember := .IsOrganizationMember}} @@ -63,7 +63,7 @@ {{if .IsOrganizationMember}}
    {{range .Teams}} diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl index 21d7c0ef3c..283b2199cb 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -1,6 +1,6 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}}
    -
    +
    {{ctx.Locale.Tr "org.settings.labels_desc"}}
    diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 593b9d454d..5e0d07ed6d 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -71,7 +71,7 @@ {{end}}

    -
    +
    {{ctx.Locale.Tr "repo.branches"}}

    diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index c8ae535a18..8f6e6e0169 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -1,5 +1,5 @@

    -
    +
    {{if or .PageIsCommits (gt .CommitCount 0)}} {{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}} {{else if .IsNothingToCompare}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 41a8268cb3..3a152a7e81 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -1,7 +1,7 @@ {{$showFileTree := (and (not .DiffNotAvailable) (gt .DiffShortStat.NumFiles 1))}}
    -
    +
    {{if $showFileTree}} {{else}} - {{$textNegitive := ctx.Locale.Tr "modal.no"}} + {{$textNegative := ctx.Locale.Tr "modal.no"}} {{$textPositive := ctx.Locale.Tr "modal.yes"}} {{if eq .ModalButtonTypes "confirm"}} - {{$textNegitive = ctx.Locale.Tr "modal.cancel"}} + {{$textNegative = ctx.Locale.Tr "modal.cancel"}} {{$textPositive = ctx.Locale.Tr "modal.confirm"}} {{end}} - {{if .ModalButtonCancelText}}{{$textNegitive = .ModalButtonCancelText}}{{end}} + {{if .ModalButtonCancelText}}{{$textNegative = .ModalButtonCancelText}}{{end}} {{if .ModalButtonOkText}}{{$textPositive = .ModalButtonOkText}}{{end}} - + {{end}}
    diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 09edcb1185..e1b7364f41 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -77,7 +77,7 @@
    -
    +
    {{range .Columns}}
    diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl index c9aac6d61d..916d589839 100644 --- a/templates/repo/diff/blob_excerpt.tmpl +++ b/templates/repo/diff/blob_excerpt.tmpl @@ -15,7 +15,7 @@ {{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}{{end}} {{if $line.LeftIdx}}{{end}} - {{/* ATTENTION: BLOB-EXCERPT-COMMENT-RIGHT: here it intentially use "right" side to comment, because the backend code depends on the assumption that the comment only happens on right side*/}} + {{/* ATTENTION: BLOB-EXCERPT-COMMENT-RIGHT: here it intentionally use "right" side to comment, because the backend code depends on the assumption that the comment only happens on right side*/}} {{- if and $canCreateComment $line.RightIdx -}}
    - + {{svg "octicon-sign-out"}} {{ctx.Locale.Tr "sign_out"}} @@ -128,7 +128,7 @@ {{end}}
    - + {{svg "octicon-sign-out"}} {{ctx.Locale.Tr "sign_out"}} diff --git a/tests/integration/signout_test.go b/tests/integration/signout_test.go index 7fd0b5c64a..0c0ac5dd87 100644 --- a/tests/integration/signout_test.go +++ b/tests/integration/signout_test.go @@ -7,7 +7,10 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" ) func TestSignOut(t *testing.T) { @@ -15,8 +18,9 @@ func TestSignOut(t *testing.T) { session := loginUser(t, "user2") - req := NewRequest(t, "POST", "/user/logout") - session.MakeRequest(t, req, http.StatusOK) + req := NewRequest(t, "GET", "/user/logout") + resp := session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) // try to view a private repo, should fail req = NewRequest(t, "GET", "/user2/repo2") From 3ee7a87c8aaea6201667afc6faaadaa706b6d525 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 07:56:23 +0000 Subject: [PATCH 030/207] Update Nix flake (#36787) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index a608aa3b89..d00e8c8501 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1771369470, - "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", + "lastModified": 1772198003, + "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0182a361324364ae3f436a63005877674cf45efb", + "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", "type": "github" }, "original": { From e3cf3601540b721b9a564ad0320ca4021a75c8eb Mon Sep 17 00:00:00 2001 From: shafi-VM Date: Sun, 1 Mar 2026 14:41:25 +0530 Subject: [PATCH 031/207] =?UTF-8?q?Add=20=E2=80=9CCopy=20Source=E2=80=9D?= =?UTF-8?q?=20to=20markup=20comment=20menu=20(#36726)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any user with **read access** to a comment can now copy its raw markdown source via the `···` context menu — no edit permission required. Closes #36722. --------- Signed-off-by: silverwind Co-authored-by: wxiaoguang Co-authored-by: silverwind Co-authored-by: Claude Opus 4.6 --- options/locale/locale_en-US.json | 1 + templates/repo/diff/comments.tmpl | 2 +- templates/repo/issue/view_content.tmpl | 2 +- templates/repo/issue/view_content/comments.tmpl | 4 ++-- .../repo/issue/view_content/context_menu.tmpl | 1 + .../repo/issue/view_content/conversation.tmpl | 2 +- web_src/js/features/clipboard.ts | 14 +++++++++++--- web_src/js/modules/tippy.ts | 9 +++++++++ 8 files changed, 27 insertions(+), 8 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 8f7a050b16..0f27c8d82d 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -1519,6 +1519,7 @@ "repo.issues.commented_at": "commented %s", "repo.issues.delete_comment_confirm": "Are you sure you want to delete this comment?", "repo.issues.context.copy_link": "Copy Link", + "repo.issues.context.copy_source": "Copy Source", "repo.issues.context.quote_reply": "Quote Reply", "repo.issues.context.reference_issue": "Reference in New Issue", "repo.issues.context.edit": "Edit", diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index f1907b7ff1..546a0229a7 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -64,7 +64,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 13e007ad95..047a9ac125 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -52,7 +52,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Issue.Content}}
    +
    {{.Issue.Content}}
    {{if .Issue.Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index a019c4bf3d..39fd822098 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -67,7 +67,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} @@ -432,7 +432,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl index 749a2fa0dd..e28d20a271 100644 --- a/templates/repo/issue/view_content/context_menu.tmpl +++ b/templates/repo/issue/view_content/context_menu.tmpl @@ -10,6 +10,7 @@ {{$referenceUrl = printf "%s/files#%s" ctx.RootData.Issue.Link .item.HashTag}} {{end}}
    {{ctx.Locale.Tr "repo.issues.context.copy_link"}}
    +
    {{ctx.Locale.Tr "repo.issues.context.copy_source"}}
    {{if ctx.RootData.IsSigned}} {{$needDivider := false}} {{if not ctx.RootData.Repository.IsArchived}} diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl index dd515933db..333d120fde 100644 --- a/templates/repo/issue/view_content/conversation.tmpl +++ b/templates/repo/issue/view_content/conversation.tmpl @@ -100,7 +100,7 @@ The variables in "ctx.Data" are different in each case, making this template fra {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} diff --git a/web_src/js/features/clipboard.ts b/web_src/js/features/clipboard.ts index d5e3b55f95..8dbaab62aa 100644 --- a/web_src/js/features/clipboard.ts +++ b/web_src/js/features/clipboard.ts @@ -6,7 +6,7 @@ const {copy_success, copy_error} = window.config.i18n; // Enable clipboard copy from HTML attributes. These properties are supported: // - data-clipboard-text: Direct text to copy -// - data-clipboard-target: Holds a selector for a or +
    +
    + + +
    + +
    + +
    + + +
    + +
    +

    + + + +{{template "admin/layout_footer" .}} diff --git a/templates/admin/badge/list.tmpl b/templates/admin/badge/list.tmpl new file mode 100644 index 0000000000..3020b7b25a --- /dev/null +++ b/templates/admin/badge/list.tmpl @@ -0,0 +1,67 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin badge")}} +
    +

    + {{ctx.Locale.Tr "admin.badges.badges_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) + +

    +
    +
    +
    + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.badge_kind")}} +
    + + +
    +
    +
    + + + + + + + + + + + {{range .Badges}} + + + + + + + {{end}} + +
    ID{{SortArrow "oldest" "newest" .SortType false}} + {{ctx.Locale.Tr "admin.badges.slug"}} + {{SortArrow "alphabetically" "reversealphabetically" $.SortType true}} + {{ctx.Locale.Tr "admin.badges.description"}}
    {{.ID}} + {{.Slug}} + {{.Description}} + +
    +
    + + {{template "base/paginate" .}} +
    +{{template "admin/layout_footer" .}} diff --git a/templates/admin/badge/new.tmpl b/templates/admin/badge/new.tmpl new file mode 100644 index 0000000000..5b67bed314 --- /dev/null +++ b/templates/admin/badge/new.tmpl @@ -0,0 +1,26 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin new badge")}} +
    +

    + {{ctx.Locale.Tr "admin.badges.new_badge"}} +

    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +{{template "admin/layout_footer" .}} diff --git a/templates/admin/badge/users.tmpl b/templates/admin/badge/users.tmpl new file mode 100644 index 0000000000..97d332010f --- /dev/null +++ b/templates/admin/badge/users.tmpl @@ -0,0 +1,40 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin badge")}} +
    +

    + {{.Title}} +

    +
    +
    + + +
    +
    + {{if .Users}} +
    +
    + {{range .Users}} +
    + +
    +
    + {{template "shared/user/name" .}} +
    +
    + +
    + {{end}} +
    +
    + {{end}} + {{template "base/paginate" .}} +
    + +{{template "admin/layout_footer" .}} diff --git a/templates/admin/badge/view.tmpl b/templates/admin/badge/view.tmpl new file mode 100644 index 0000000000..efd31f4c41 --- /dev/null +++ b/templates/admin/badge/view.tmpl @@ -0,0 +1,44 @@ +{{template "admin/layout_head" (dict "ctxData" .)}} + +
    +
    +
    +

    + {{.Title}} + +

    +
    +
    +
    + {{if .Badge.ImageURL}} +
    + {{.Badge.Description}} +
    + {{end}} +
    +
    + {{.Badge.Slug}} +
    +
    + {{.Badge.Description}} +
    +
    +
    +
    +
    +
    +
    +

    + {{ctx.Locale.Tr "explore.users"}} ({{.UsersTotal}}) + +

    +
    + {{template "explore/user_list" .}} +
    +
    + +{{template "admin/layout_footer" .}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 72584ec799..ce3048ed9f 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -13,7 +13,7 @@
    -
    +
    {{ctx.Locale.Tr "admin.identity_access"}} {{end}} {{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}} diff --git a/web_src/css/user.css b/web_src/css/user.css index a9b283b504..8685fdf914 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -92,15 +92,31 @@ } .user-badges { - display: grid; - grid-template-columns: repeat(auto-fill, 64px); - gap: 2px; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0; + min-width: 0; +} + +.user-badge-item { + display: inline-flex; + flex: 0 0 auto; + min-width: max-content; } .user-badges img { object-fit: contain; } +.user-badge-chip { + max-width: none !important; + overflow: visible !important; + text-overflow: clip !important; + white-space: nowrap; + min-width: max-content; +} + #readme_profile { padding: 1em 2em; border-radius: var(--border-radius); From ae0bc0222a2b134b0c4106fa9a90b1635a06fcc8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 08:49:25 +0100 Subject: [PATCH 101/207] Update to eslint 10 (#36925) - Enable a few more rules, fix issues. The 2 `value` issues are false-positives. - Add exact types for `window.pageData` and `window.notificationSettings`. - peerDependencyRules for eslint-plugin-github unrestricted, the plugin works in v10, but does not declare compatibility, pending https://github.com/github/eslint-plugin-github/issues/680. - Added [eslint-plugin-de-morgan](https://github.com/azat-io/eslint-plugin-de-morgan), no violations. --------- Signed-off-by: silverwind Signed-off-by: wxiaoguang Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang Co-authored-by: Lunny Xiao --- eslint.config.ts | 25 +- package.json | 26 +- pnpm-lock.yaml | 562 +++++++++++------- updates.config.ts | 2 - web_src/js/components/DashboardRepoList.vue | 75 ++- .../js/components/PullRequestMergeForm.vue | 2 +- web_src/js/components/RepoCodeFrequency.vue | 2 +- web_src/js/components/RepoRecentCommits.vue | 2 +- web_src/js/features/citation.ts | 2 +- web_src/js/features/common-fetch-action.ts | 2 +- web_src/js/features/pull-view-file.ts | 9 +- web_src/js/features/repo-search.ts | 2 +- web_src/js/globals.d.ts | 26 +- web_src/js/modules/diff-file.ts | 4 +- web_src/js/utils.ts | 12 +- web_src/js/vitest.setup.ts | 2 +- 16 files changed, 494 insertions(+), 261 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 8ed0cf789a..9f98adf859 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,5 +1,6 @@ import arrayFunc from 'eslint-plugin-array-func'; import comments from '@eslint-community/eslint-plugin-eslint-comments'; +import deMorgan from 'eslint-plugin-de-morgan'; import github from 'eslint-plugin-github'; import globals from 'globals'; import importPlugin from 'eslint-plugin-import-x'; @@ -63,6 +64,7 @@ export default defineConfig([ '@stylistic': stylistic, '@typescript-eslint': typescriptPlugin.plugin, 'array-func': arrayFunc, + 'de-morgan': deMorgan, 'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203 regexp, sonarjs, @@ -179,7 +181,7 @@ export default defineConfig([ '@typescript-eslint/naming-convention': [0], '@typescript-eslint/no-array-constructor': [2], '@typescript-eslint/no-array-delete': [2], - '@typescript-eslint/no-base-to-string': [0], + '@typescript-eslint/no-base-to-string': [2], '@typescript-eslint/no-confusing-non-null-assertion': [2], '@typescript-eslint/no-confusing-void-expression': [0], '@typescript-eslint/no-deprecated': [2], @@ -254,10 +256,10 @@ export default defineConfig([ '@typescript-eslint/prefer-function-type': [2], '@typescript-eslint/prefer-includes': [2], '@typescript-eslint/prefer-literal-enum-member': [0], - '@typescript-eslint/prefer-namespace-keyword': [0], + '@typescript-eslint/prefer-namespace-keyword': [2], '@typescript-eslint/prefer-nullish-coalescing': [0], '@typescript-eslint/prefer-optional-chain': [2, {requireNullish: true}], - '@typescript-eslint/prefer-promise-reject-errors': [0], + '@typescript-eslint/prefer-promise-reject-errors': [2], '@typescript-eslint/prefer-readonly': [0], '@typescript-eslint/prefer-readonly-parameter-types': [0], '@typescript-eslint/prefer-reduce-type-parameter': [0], @@ -268,7 +270,7 @@ export default defineConfig([ '@typescript-eslint/require-array-sort-compare': [0], '@typescript-eslint/require-await': [0], '@typescript-eslint/restrict-plus-operands': [2], - '@typescript-eslint/restrict-template-expressions': [0], + '@typescript-eslint/restrict-template-expressions': [2], '@typescript-eslint/return-await': [0], '@typescript-eslint/strict-boolean-expressions': [0], '@typescript-eslint/strict-void-return': [0], @@ -295,6 +297,8 @@ export default defineConfig([ 'consistent-this': [0], 'constructor-super': [2], 'curly': [0], + 'de-morgan/no-negated-conjunction': [2], + 'de-morgan/no-negated-disjunction': [2], 'default-case-last': [2], 'default-case': [0], 'default-param-last': [0], @@ -586,6 +590,7 @@ export default defineConfig([ 'no-undef-init': [2], 'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time 'no-undefined': [0], + 'no-unassigned-vars': [2], 'no-underscore-dangle': [0], 'no-unexpected-multiline': [2], 'no-unmodified-loop-condition': [2], @@ -626,19 +631,20 @@ export default defineConfig([ 'prefer-numeric-literals': [2], 'prefer-object-has-own': [2], 'prefer-object-spread': [2], - 'prefer-promise-reject-errors': [2, {allowEmptyReject: false}], + 'prefer-promise-reject-errors': [0], // handled by @typescript-eslint/prefer-promise-reject-errors 'prefer-regex-literals': [2], 'prefer-rest-params': [2], 'prefer-spread': [2], 'prefer-template': [2], - 'radix': [2, 'as-needed'], + 'preserve-caught-error': [0], + 'radix': [0], 'regexp/confusing-quantifier': [2], 'regexp/control-character-escape': [2], 'regexp/hexadecimal-escape': [0], 'regexp/letter-case': [0], 'regexp/match-any': [2], 'regexp/negation': [2], - 'regexp/no-contradiction-with-assertion': [0], + 'regexp/no-contradiction-with-assertion': [2], 'regexp/no-control-character': [0], 'regexp/no-dupe-characters-character-class': [2], 'regexp/no-dupe-disjunctions': [2], @@ -654,8 +660,8 @@ export default defineConfig([ 'regexp/no-invisible-character': [2], 'regexp/no-lazy-ends': [2], 'regexp/no-legacy-features': [2], - 'regexp/no-misleading-capturing-group': [0], - 'regexp/no-misleading-unicode-character': [0], + 'regexp/no-misleading-capturing-group': [2], + 'regexp/no-misleading-unicode-character': [2], 'regexp/no-missing-g-flag': [2], 'regexp/no-non-standard-flag': [2], 'regexp/no-obscure-range': [2], @@ -991,6 +997,7 @@ export default defineConfig([ 'vitest/require-top-level-describe': [0], 'vitest/valid-describe-callback': [2], 'vitest/valid-expect': [2, {maxArgs: 2}], + 'vitest/valid-expect-in-promise': [2], 'vitest/valid-title': [2], }, }, diff --git a/package.json b/package.json index 11dc64ee6a..4dd3f14e06 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.7.1", - "@eslint/json": "0.14.0", + "@eslint/json": "1.1.0", "@playwright/test": "1.58.2", "@stylistic/eslint-plugin": "5.10.0", "@stylistic/stylelint-plugin": "5.0.1", @@ -82,17 +82,18 @@ "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/parser": "8.57.1", "@vitejs/plugin-vue": "6.0.4", - "@vitest/eslint-plugin": "1.6.9", - "eslint": "9.39.2", + "@vitest/eslint-plugin": "1.6.12", + "eslint": "10.0.3", "eslint-import-resolver-typescript": "4.4.4", - "eslint-plugin-array-func": "5.1.0", + "eslint-plugin-array-func": "5.1.1", "eslint-plugin-github": "6.0.0", - "eslint-plugin-import-x": "4.16.1", - "eslint-plugin-playwright": "2.9.0", - "eslint-plugin-regexp": "3.0.0", - "eslint-plugin-sonarjs": "4.0.1", + "eslint-plugin-de-morgan": "2.1.1", + "eslint-plugin-import-x": "4.16.2", + "eslint-plugin-playwright": "2.10.1", + "eslint-plugin-regexp": "3.1.0", + "eslint-plugin-sonarjs": "4.0.2", "eslint-plugin-unicorn": "63.0.0", "eslint-plugin-vue": "10.8.0", "eslint-plugin-vue-scoped-css": "3.0.0", @@ -112,13 +113,18 @@ "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", "typescript": "5.9.3", - "typescript-eslint": "8.56.1", + "typescript-eslint": "8.57.1", "updates": "17.8.3", "vite-string-plugin": "2.0.1", "vitest": "4.0.18", "vue-tsc": "3.2.5" }, "pnpm": { + "peerDependencyRules": { + "allowedVersions": { + "eslint-plugin-github>eslint": ">=9" + } + }, "overrides": { "array-includes": "npm:@nolyfill/array-includes@^1", "array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@^1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 553364a5bc..5bb595bf0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,16 +203,16 @@ importers: devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.1 - version: 4.7.1(eslint@9.39.2(jiti@2.6.1)) + version: 4.7.1(eslint@10.0.3(jiti@2.6.1)) '@eslint/json': - specifier: 0.14.0 - version: 0.14.0 + specifier: 1.1.0 + version: 1.1.0 '@playwright/test': specifier: 1.58.2 version: 1.58.2 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@9.39.2(jiti@2.6.1)) + version: 5.10.0(eslint@10.0.3(jiti@2.6.1)) '@stylistic/stylelint-plugin': specifier: 5.0.1 version: 5.0.1(stylelint@17.4.0(typescript@5.9.3)) @@ -250,50 +250,53 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.56.1 - version: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.57.1 + version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-vue': specifier: 6.0.4 version: 6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) '@vitest/eslint-plugin': - specifier: 1.6.9 - version: 1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + specifier: 1.6.12 + version: 1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) eslint: - specifier: 9.39.2 - version: 9.39.2(jiti@2.6.1) + specifier: 10.0.3 + version: 10.0.3(jiti@2.6.1) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-array-func: - specifier: 5.1.0 - version: 5.1.0(eslint@9.39.2(jiti@2.6.1)) + specifier: 5.1.1 + version: 5.1.1(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-de-morgan: + specifier: 2.1.1 + version: 2.1.1(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-github: specifier: 6.0.0 - version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-import-x: - specifier: 4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) + specifier: 4.16.2 + version: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-playwright: - specifier: 2.9.0 - version: 2.9.0(eslint@9.39.2(jiti@2.6.1)) + specifier: 2.10.1 + version: 2.10.1(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-regexp: - specifier: 3.0.0 - version: 3.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: 3.1.0 + version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-sonarjs: - specifier: 4.0.1 - version: 4.0.1(eslint@9.39.2(jiti@2.6.1)) + specifier: 4.0.2 + version: 4.0.2(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-unicorn: specifier: 63.0.0 - version: 63.0.0(eslint@9.39.2(jiti@2.6.1)) + version: 63.0.0(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-vue: specifier: 10.8.0 - version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@9.39.2(jiti@2.6.1)))(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1))) + version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) eslint-plugin-vue-scoped-css: specifier: 3.0.0 - version: 3.0.0(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1))) + version: 3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@9.39.2(jiti@2.6.1)) + version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) globals: specifier: 17.4.0 version: 17.4.0 @@ -340,8 +343,8 @@ importers: specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: 8.56.1 - version: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.57.1 + version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) updates: specifier: 17.8.3 version: 17.8.3 @@ -694,41 +697,41 @@ packages: eslint: optional: true - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.3': + resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.5.3': + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.1.1': + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/eslintrc@3.3.4': resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.3': resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/json@0.14.0': - resolution: {integrity: sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/json@1.1.0': + resolution: {integrity: sha512-noH9FUYqyhZSDf3Yq5HswsjDH/MWJAatMooWwT5YgQ0XHMekoFc/iyEufP+7kD1kaOj9qwFiXySqHsKii3zmlw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.3': + resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@github/browserslist-config@1.0.0': resolution: {integrity: sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==} @@ -887,6 +890,9 @@ packages: resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} engines: {node: '>=12.4.0'} + '@package-json/types@0.0.12': + resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1296,8 +1302,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.56.1': - resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + '@typescript-eslint/eslint-plugin@8.57.1': + resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.57.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.57.1': + resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1309,16 +1323,32 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.57.1': + resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.56.1': resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.57.1': + resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.56.1': resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.57.1': + resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.56.1': resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1326,16 +1356,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.57.1': + resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@8.56.1': resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.57.1': + resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.56.1': resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.57.1': + resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.56.1': resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1343,10 +1390,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.57.1': + resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.56.1': resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.57.1': + resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -1457,8 +1515,8 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.9': - resolution: {integrity: sha512-9WfPx1OwJ19QLCSRLkqVO7//1WcWnK3fE/3fJhKMAmDe8+9G4rB47xCNIIeCq3FdEzkIoLTfDlwDlPBaUTMhow==} + '@vitest/eslint-plugin@1.6.12': + resolution: {integrity: sha512-4kI47BJNFE+EQ5bmPbHzBF+ibNzx2Fj0Jo9xhWsTPxMddlHwIWl6YAxagefh461hrwx/W0QwBZpxGS404kBXyg==} engines: {node: '>=18'} peerDependencies: eslint: '>=8.57.0' @@ -2363,12 +2421,18 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-array-func@5.1.0: - resolution: {integrity: sha512-+OULB0IQdENBmBf8pHMPPObgV6QyfeXFin483jPonOaiurI9UFmc8UydWriK5f5Gel8xBhQLA6NzMwbck1BUJw==} + eslint-plugin-array-func@5.1.1: + resolution: {integrity: sha512-TbVGk+yLqXHgtrS4DnYzg2Ycuk5y+lYFy5NgT748neQdJvNIYUucxp2QQjPU7dwbs9xp9fyktgtK069y9rNdig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=8.51.0' + eslint-plugin-de-morgan@2.1.1: + resolution: {integrity: sha512-0CeQ38b8hMMa3gO5vLnGcQS/xatvYno9RvjKoZ4UVaHjzvHZhR9i6+0ZkZhbbZF1FW7rqrS2MtcH3tVBejrmHQ==} + engines: {node: ^20.0.0 || >=22.0.0} + peerDependencies: + eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 + eslint-plugin-escompat@3.11.4: resolution: {integrity: sha512-j0ywwNnIufshOzgAu+PfIig1c7VRClKSNKzpniMT2vXQ4leL5q+e/SpMFQU0nrdL2WFFM44XmhSuwmxb3G0CJg==} peerDependencies: @@ -2396,12 +2460,12 @@ packages: peerDependencies: eslint: '>=5.0.0' - eslint-plugin-import-x@4.16.1: - resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==} + eslint-plugin-import-x@4.16.2: + resolution: {integrity: sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/utils': ^8.0.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/utils': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint-import-resolver-node: '*' peerDependenciesMeta: '@typescript-eslint/utils': @@ -2429,8 +2493,8 @@ packages: resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} engines: {node: '>=5.0.0'} - eslint-plugin-playwright@2.9.0: - resolution: {integrity: sha512-k3xrG6YzrallWNFMoGUjMNeu3SFFKXN79KJQBD2PkM4PasJegqV2Up+mPY5od2UmPKQGT+MeIhCmWH8r5eYuQQ==} + eslint-plugin-playwright@2.10.1: + resolution: {integrity: sha512-qea3UxBOb8fTwJ77FMApZKvRye5DOluDHcev0LDJwID3RELeun0JlqzrNIXAB/SXCyB/AesCW/6sZfcT9q3Edg==} engines: {node: '>=16.9.0'} peerDependencies: eslint: '>=8.40.0' @@ -2449,14 +2513,14 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-regexp@3.0.0: - resolution: {integrity: sha512-iW7hgAV8NOG6E2dz+VeKpq67YLQ9jaajOKYpoOSic2/q8y9BMdXBKkSR9gcMtbqEhNQzdW41E3wWzvhp8ExYwQ==} + eslint-plugin-regexp@3.1.0: + resolution: {integrity: sha512-qGXIC3DIKZHcK1H9A9+Byz9gmndY6TTSRkSMTZpNXdyCw2ObSehRgccJv35n9AdUakEjQp5VFNLas6BMXizCZg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: '>=9.38.0' - eslint-plugin-sonarjs@4.0.1: - resolution: {integrity: sha512-lmqzFTrw0/zpHQMRmwdgdEEw50s3md0c8RE23JqNom9ovsGQxC/azZ9H00aGKVDkxIXywfcxwzyFJ9Sm3bp2ng==} + eslint-plugin-sonarjs@4.0.2: + resolution: {integrity: sha512-BTcT1zr1iTbmJtVlcesISwnXzh+9uhf9LEOr+RRNf4kR8xA0HQTPft4oiyOCzCOGKkpSJxjR8ZYF6H7VPyplyw==} peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -2507,14 +2571,14 @@ packages: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@9.1.1: resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2527,9 +2591,9 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@10.0.3: + resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -4000,8 +4064,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.56.1: - resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + typescript-eslint@8.57.1: + resolution: {integrity: sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4555,41 +4619,45 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.0.3(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint/compat@1.4.1(eslint@10.0.3(jiti@2.6.1))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) - '@eslint/config-array@0.21.2': + '@eslint/config-array@0.23.3': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.3 debug: 4.4.3 - minimatch: 3.1.5 + minimatch: 10.2.4 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.5.3': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.1 '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@1.1.1': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.4': dependencies: ajv: 6.14.0 @@ -4604,22 +4672,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.2': {} - '@eslint/js@9.39.3': {} - '@eslint/json@0.14.0': + '@eslint/json@1.1.0': dependencies: - '@eslint/core': 0.17.0 - '@eslint/plugin-kit': 0.4.1 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 '@humanwhocodes/momoa': 3.3.10 natural-compare: 1.4.0 - '@eslint/object-schema@2.1.7': {} + '@eslint/object-schema@3.0.3': {} - '@eslint/plugin-kit@0.4.1': + '@eslint/plugin-kit@0.6.1': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.1 levn: 0.4.1 '@github/browserslist-config@1.0.0': {} @@ -4770,6 +4836,8 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 + '@package-json/types@0.0.12': {} + '@pkgr/core@0.2.9': {} '@playwright/test@1.58.2': @@ -4892,11 +4960,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@9.39.2(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) '@typescript-eslint/types': 8.56.1 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -5130,15 +5198,15 @@ snapshots: dependencies: '@types/node': 25.3.5 - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -5146,14 +5214,30 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + eslint: 10.0.3(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5167,22 +5251,52 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/scope-manager@8.57.1': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.0.3(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -5190,6 +5304,8 @@ snapshots: '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/types@8.57.1': {} + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) @@ -5205,13 +5321,39 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + eslint: 10.0.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5221,6 +5363,11 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.57.1': + dependencies: + '@typescript-eslint/types': 8.57.1 + eslint-visitor-keys: 5.0.1 + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -5286,11 +5433,11 @@ snapshots: vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) vue: 3.5.29(typescript@5.9.3) - '@vitest/eslint-plugin@1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': dependencies: '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.3(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 vitest: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) @@ -6181,9 +6328,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: @@ -6200,10 +6347,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.6 is-bun-module: 2.0.0 @@ -6211,87 +6358,92 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-array-func@5.1.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-array-func@5.1.1(eslint@10.0.3(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) - eslint-plugin-escompat@3.11.4(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-de-morgan@2.1.1(eslint@10.0.3(jiti@2.6.1)): + dependencies: + eslint: 10.0.3(jiti@2.6.1) + + eslint-plugin-escompat@3.11.4(eslint@10.0.3(jiti@2.6.1)): dependencies: browserslist: 4.28.1 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) - eslint-plugin-eslint-comments@3.2.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-eslint-comments@3.2.0(eslint@10.0.3(jiti@2.6.1)): dependencies: escape-string-regexp: 1.0.5 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-filenames@1.3.2(eslint@10.0.3(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): dependencies: - '@eslint/compat': 1.4.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint/compat': 1.4.1(eslint@10.0.3(jiti@2.6.1)) '@eslint/eslintrc': 3.3.4 '@eslint/js': 9.39.3 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 9.39.2(jiti@2.6.1) - eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-escompat: 3.11.4(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-eslint-comments: 3.2.0(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-filenames: 1.3.2(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-i18n-text: 1.0.1(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) + eslint: 10.0.3(jiti@2.6.1) + eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-escompat: 3.11.4(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-filenames: 1.3.2(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-i18n-text: 1.0.1(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.0.3(jiti@2.6.1)) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) + eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1) eslint-rule-documentation: 1.0.23 globals: 16.5.0 jsx-ast-utils: 3.3.5 prettier: 3.8.1 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-i18n-text@1.0.1(eslint@10.0.3(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)): dependencies: + '@package-json/types': 0.0.12 '@typescript-eslint/types': 8.56.1 comment-parser: 1.4.5 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.4 @@ -6299,12 +6451,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6313,9 +6465,9 @@ snapshots: array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) hasown: '@nolyfill/hasown@1.0.44' is-core-module: '@nolyfill/is-core-module@1.0.39' is-glob: 4.0.3 @@ -6327,13 +6479,13 @@ snapshots: string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.0.3(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6343,7 +6495,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) hasown: '@nolyfill/hasown@1.0.44' jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6354,38 +6506,38 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-playwright@2.9.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-playwright@2.10.1(eslint@10.0.3(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) globals: 17.4.0 - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) prettier: 3.8.1 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-regexp@3.0.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-regexp@3.1.0(eslint@10.0.3(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.5 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) jsdoc-type-pratt-parser: 7.1.1 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-sonarjs@4.0.2(eslint@10.0.3(jiti@2.6.1)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) functional-red-black-tree: 1.0.1 globals: 17.4.0 jsx-ast-utils-x: 0.1.0 @@ -6396,15 +6548,15 @@ snapshots: ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 - eslint-plugin-unicorn@63.0.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-unicorn@63.0.0(eslint@10.0.3(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.48.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) find-up-simple: 1.0.1 globals: 16.5.0 indent-string: 5.0.0 @@ -6416,33 +6568,33 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.0.0(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1))): + eslint-plugin-vue-scoped-css@3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + eslint: 10.0.3(jiti@2.6.1) lodash: 4.17.23 postcss: 8.5.8 postcss-safe-parser: 7.0.1(postcss@8.5.8) postcss-selector-parser: 7.1.1 - vue-eslint-parser: 10.4.0(eslint@9.39.2(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@9.39.2(jiti@2.6.1)))(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1))): + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + eslint: 10.0.3(jiti@2.6.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@9.39.2(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.0.3(jiti@2.6.1)) + '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-wc@3.1.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-wc@3.1.0(eslint@10.0.3(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 @@ -6453,12 +6605,14 @@ snapshots: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@8.4.0: + eslint-scope@9.1.1: dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-scope@9.1.1: + eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 '@types/estree': 1.0.8 @@ -6471,28 +6625,25 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.2(jiti@2.6.1): + eslint@10.0.3(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.4 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.3 + '@eslint/config-helpers': 0.5.3 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 ajv: 6.14.0 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.1.1 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -6503,8 +6654,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 + minimatch: 10.2.4 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -8033,13 +8183,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8185,10 +8335,10 @@ snapshots: chart.js: 4.5.1 vue: 3.5.29(typescript@5.9.3) - vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1)): + vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.3(jiti@2.6.1) eslint-scope: 9.1.1 eslint-visitor-keys: 5.0.1 espree: 11.1.1 diff --git a/updates.config.ts b/updates.config.ts index bc9e368fb4..a7eb364b44 100644 --- a/updates.config.ts +++ b/updates.config.ts @@ -4,8 +4,6 @@ export default { exclude: [ '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled 'cropperjs', // need to migrate to v2 but v2 is not compatible with v1 - 'eslint', // need to migrate to v10 'tailwindcss', // need to migrate - '@eslint/json', // needs eslint 10 ], } satisfies Config; diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index e795d647b9..5b0842336f 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -3,14 +3,30 @@ import {nextTick, defineComponent} from 'vue'; import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import type {SvgName} from '../svg.ts'; const {appSubUrl, assetUrlPrefix, pageData} = window.config; +type DashboardRepo = { + id: number, + link: string, + full_name: string, + archived: boolean, + fork: boolean, + mirror: boolean, + template: boolean, + private: boolean, + internal: boolean, + latest_commit_status_state?: CommitStatus, + latest_commit_status_state_link?: string, + locale_latest_commit_status_state?: string, +}; + type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped'; type CommitStatusMap = { [status in CommitStatus]: { - name: string, + name: SvgName, color: string, }; }; @@ -38,8 +54,8 @@ export default defineComponent({ return { tab, - repos: [], - reposTotalCount: null, + repos: [] as DashboardRepo[], + reposTotalCount: null as number | null, reposFilter, archivedFilter, privateFilter, @@ -48,7 +64,7 @@ export default defineComponent({ searchQuery, isLoading: false, staticPrefix: assetUrlPrefix, - counts: {}, + counts: {} as Record, repoTypes: { all: { searchMode: '', @@ -65,16 +81,49 @@ export default defineComponent({ collaborative: { searchMode: 'collaborative', }, - }, - textArchivedFilterTitles: {}, - textPrivateFilterTitles: {}, - - organizations: [], + } as Record, + textArchivedFilterTitles: {} as Record, + textPrivateFilterTitles: {} as Record, + organizations: [] as Array<{name: string, full_name: string, num_repos: number, org_visibility: string}>, isOrganization: true, canCreateOrganization: false, organizationsTotalCount: 0, organizationId: 0, - + searchLimit: 0, + uid: 0, + teamId: 0, + isMirrorsEnabled: false, + isStarsEnabled: false, + canCreateMigrations: false, + textNoOrg: '', + textNoRepo: '', + textRepository: '', + textOrganization: '', + textMyRepos: '', + textNewRepo: '', + textSearchRepos: '', + textFilter: '', + textShowArchived: '', + textShowPrivate: '', + textShowBothArchivedUnarchived: '', + textShowOnlyUnarchived: '', + textShowOnlyArchived: '', + textShowBothPrivatePublic: '', + textShowOnlyPublic: '', + textShowOnlyPrivate: '', + textAll: '', + textSources: '', + textForks: '', + textMirrors: '', + textCollaborative: '', + textFirstPage: '', + textPreviousPage: '', + textNextPage: '', + textLastPage: '', + textMyOrgs: '', + textNewOrg: '', + textOrgVisibilityLimited: '', + textOrgVisibilityPrivate: '', subUrl: appSubUrl, ...pageData.dashboardRepoList, activeIndex: -1, // don't select anything at load, first cursor down will select @@ -250,7 +299,7 @@ export default defineComponent({ nextTick(() => { // MDN: If there's no focused element, this is the Document.body or Document.documentElement. if ((document.activeElement === document.body || document.activeElement === document.documentElement)) { - this.$refs.search.focus({preventScroll: true}); + (this.$refs.search as HTMLInputElement).focus({preventScroll: true}); } }); } @@ -283,7 +332,7 @@ export default defineComponent({ } }, - repoIcon(repo: any) { + repoIcon(repo: DashboardRepo) { if (repo.fork) { return 'octicon-repo-forked'; } else if (repo.mirror) { @@ -435,7 +484,7 @@ export default defineComponent({
    - + diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index e05046dadd..903c7d9a9d 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -5,7 +5,7 @@ import {toggleElem} from '../utils/dom.ts'; const {pageData} = window.config; -const mergeForm = pageData.pullRequestMergeForm; +const mergeForm = pageData.pullRequestMergeForm!; const mergeTitleFieldValue = shallowRef(''); const mergeMessageFieldValue = shallowRef(''); diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue index d85083922e..6cba3c5109 100644 --- a/web_src/js/components/RepoCodeFrequency.vue +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -49,7 +49,7 @@ defineProps<{ const isLoading = shallowRef(false); const errorText = shallowRef(''); -const repoLink = pageData.repoLink; +const repoLink = pageData.repoLink!; const data = shallowRef([]); onMounted(() => { diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue index e1629e3829..12b54cc87f 100644 --- a/web_src/js/components/RepoRecentCommits.vue +++ b/web_src/js/components/RepoRecentCommits.vue @@ -46,7 +46,7 @@ defineProps<{ const isLoading = shallowRef(false); const errorText = shallowRef(''); -const repoLink = pageData.repoLink; +const repoLink = pageData.repoLink!; const data = ref([]); onMounted(() => { diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts index 79d932eab4..6d30d81685 100644 --- a/web_src/js/features/citation.ts +++ b/web_src/js/features/citation.ts @@ -11,7 +11,7 @@ async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citati import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'), import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'), ]); - const {citationFileContent} = pageData; + const citationFileContent = pageData.citationFileContent!; const config = plugins.config.get('@bibtex'); config.constants.fieldTypes.doi = ['field', 'literal']; config.constants.fieldTypes.version = ['field', 'literal']; diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 0d72fb32c9..7d98da17f2 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -99,7 +99,7 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, opts: Submi if (formMethod.toLowerCase() === 'get') { const params = new URLSearchParams(); for (const [key, value] of formData) { - params.append(key, value.toString()); + params.append(key, value as string); } const pos = reqUrl.indexOf('?'); if (pos !== -1) { diff --git a/web_src/js/features/pull-view-file.ts b/web_src/js/features/pull-view-file.ts index eca582d10a..d93d52549f 100644 --- a/web_src/js/features/pull-view-file.ts +++ b/web_src/js/features/pull-view-file.ts @@ -3,7 +3,8 @@ import {setFileFolding} from './file-fold.ts'; import {POST} from '../modules/fetch.ts'; const {pageData} = window.config; -const prReview = pageData.prReview || {}; +// it is undefined on most pages, fortunately, when it is accessed by the related functions, it exists +const prReview = pageData.prReview!; const viewedStyleClass = 'viewed-file-checked-form'; const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found const expandFilesBtnSelector = '#expand-files-btn'; @@ -13,11 +14,11 @@ const collapseFilesBtnSelector = '#collapse-files-btn'; // The data used will be window.config.pageData.prReview.numberOf{Viewed}Files function refreshViewedFilesSummary() { const viewedFilesProgress = document.querySelector('#viewed-files-summary')!; - viewedFilesProgress.setAttribute('value', prReview.numberOfViewedFiles); + viewedFilesProgress.setAttribute('value', String(prReview.numberOfViewedFiles)); const summaryLabel = document.querySelector('#viewed-files-summary-label')!; summaryLabel.textContent = summaryLabel.getAttribute('data-text-changed-template')! - .replace('%[1]d', prReview.numberOfViewedFiles) - .replace('%[2]d', prReview.numberOfFiles); + .replace('%[1]d', String(prReview.numberOfViewedFiles)) + .replace('%[2]d', String(prReview.numberOfFiles)); } // Initializes a listener for all children of the given html element diff --git a/web_src/js/features/repo-search.ts b/web_src/js/features/repo-search.ts index d3a40286fe..8fde3c9897 100644 --- a/web_src/js/features/repo-search.ts +++ b/web_src/js/features/repo-search.ts @@ -7,7 +7,7 @@ export function initRepositorySearch() { const params = new URLSearchParams(); for (const [key, value] of new FormData(repositorySearchForm).entries()) { - params.set(key, value.toString()); + params.set(key, value as string); } if ((e.target as HTMLInputElement).name === 'clear-filter') { params.delete('archived'); diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index ff025efbf5..f6e0a109b0 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -26,8 +26,30 @@ interface Window { assetUrlPrefix: string, runModeIsProd: boolean, customEmojis: Record, - pageData: Record, - notificationSettings: Record, + pageData: Record & { + adminUserListSearchForm?: { + SortType: string, + StatusFilterMap: Record, + }, + citationFileContent?: string, + prReview?: { + numberOfFiles: number, + numberOfViewedFiles: number, + }, + DiffFileTree?: import('./modules/diff-file.ts').DiffFileTreeData, + FolderIcon?: string, + FolderOpenIcon?: string, + repoLink?: string, + repoActivityTopAuthors?: any[], + pullRequestMergeForm?: Record, + dashboardRepoList?: Record, + }, + notificationSettings: { + MinTimeout: number, + TimeoutStep: number, + MaxTimeout: number, + EventSourceUpdateTime: number, + }, enableTimeTracking: boolean, mermaidMaxSourceCharacters: number, i18n: Record, diff --git a/web_src/js/modules/diff-file.ts b/web_src/js/modules/diff-file.ts index 25fc327a54..18d085b89c 100644 --- a/web_src/js/modules/diff-file.ts +++ b/web_src/js/modules/diff-file.ts @@ -17,7 +17,7 @@ export type DiffTreeEntry = { ParentEntry?: DiffTreeEntry, }; -type DiffFileTreeData = { +export type DiffFileTreeData = { TreeRoot: DiffTreeEntry, }; @@ -33,7 +33,7 @@ type DiffFileTree = { let diffTreeStoreReactive: Reactive; export function diffTreeStore() { if (!diffTreeStoreReactive) { - diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree, pageData.FolderIcon, pageData.FolderOpenIcon); + diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree!, pageData.FolderIcon!, pageData.FolderOpenIcon!); } return diffTreeStoreReactive; } diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index 6a31e767bb..1edd17fbb4 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -112,8 +112,8 @@ export function blobToDataURI(blob: Blob): Promise { reject(new Error('blobToDataURI: FileReader error')); }); reader.readAsDataURL(blob); - } catch (err) { - reject(err); + } catch (err: unknown) { + reject(err instanceof Error ? err : new Error(String(err))); } }); } @@ -135,16 +135,16 @@ export function convertImage(blob: Blob, mime: string): Promise { if (!(blob instanceof Blob)) return reject(new Error('convertImage: toBlob failed')); resolve(blob); }, mime); - } catch (err) { - reject(err); + } catch (err: unknown) { + reject(err instanceof Error ? err : new Error(String(err))); } }); img.addEventListener('error', () => { reject(new Error('convertImage: image failed to load')); }); img.src = await blobToDataURI(blob); - } catch (err) { - reject(err); + } catch (err: unknown) { + reject(err instanceof Error ? err : new Error(String(err))); } }); } diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index ff8efb697a..76ba208e00 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -8,7 +8,7 @@ window.config = { runModeIsProd: true, customEmojis: {}, pageData: {}, - notificationSettings: {}, + notificationSettings: {MinTimeout: 0, TimeoutStep: 0, MaxTimeout: 0, EventSourceUpdateTime: 0}, enableTimeTracking: true, mermaidMaxSourceCharacters: 5000, i18n: {}, From 1edbc21fcc978acd14d526c65b380fe7dc0548cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:28:30 +0000 Subject: [PATCH 102/207] Update Nix flake (#36943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated changes by the [update-flake-lock](https://github.com/DeterminateSystems/update-flake-lock) GitHub Action. ``` Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/c06b4ae' (2026-03-13) → 'github:nixos/nixpkgs/b40629e' (2026-03-18) ``` ### Running GitHub Actions on this PR GitHub Actions will not run workflows on pull requests which are opened by a GitHub Action. **To run GitHub Actions workflows on this PR, close and re-open this pull request.** Co-authored-by: github-actions[bot] --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 3af5793842..8c7ac0c196 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1773389992, - "narHash": "sha256-wvfdLLWJ2I9oEpDd9PfMA8osfIZicoQ5MT1jIwNs9Tk=", + "lastModified": 1773821835, + "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", + "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "type": "github" }, "original": { From ef88cdb7e73fdb626d3b8ce4c7efc938f51c8fea Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 18:34:45 +0100 Subject: [PATCH 103/207] Add `DEFAULT_DELETE_BRANCH_AFTER_MERGE` setting (#36917) Add this config option, applying to new repos: ```ini [repository.pull-request] DEFAULT_DELETE_BRANCH_AFTER_MERGE = true ``` Defaults to `false`, preserving current behavior. --------- Co-authored-by: Claude (Opus 4.6) --- custom/conf/app.example.ini | 3 +++ models/repo/repo_unit.go | 1 + modules/setting/repository.go | 2 ++ 3 files changed, 6 insertions(+) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 9297f3d062..803231ff12 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1162,6 +1162,9 @@ LEVEL = Info ;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated. ;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks. ;DELAY_CHECK_FOR_INACTIVE_DAYS = 7 +;; +;; Set the default value for "Delete pull request branch after merge by default" for new repositories +;DEFAULT_DELETE_BRANCH_AFTER_MERGE = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 797b34de69..18ebd6be54 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -142,6 +142,7 @@ func DefaultPullRequestsConfig() *PullRequestsConfig { AllowRebaseUpdate: true, DefaultAllowMaintainerEdit: true, } + cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle) cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge) return cfg diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 662e03598b..f4e45d4702 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -89,6 +89,7 @@ var ( TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int + DefaultDeleteBranchAfterMerge bool } `ini:"repository.pull-request"` // Issue Setting @@ -213,6 +214,7 @@ var ( TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int + DefaultDeleteBranchAfterMerge bool }{ WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, // Same as GitHub. See From 788200de9ffa7aa4fb172a882b75276fc5ece0b0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 18:41:04 +0100 Subject: [PATCH 104/207] Rework checkbox styling, remove `input` border hover effect (#36870) - Rework all checkbox styling to be consistent inside and outside markup. - Remove `input` border hover effect. Was too subtle and honestly unneeded, consistent with GitHub. - Increase `input` border contrast slightly. - Some small spacing fixes in Markup (nested tasklist and spacing after checkbox). Screenshot 2026-03-09 at 08 18 19 Screenshot 2026-03-09 at 08 18 10 Screenshot 2026-03-09 at 08 17 32 Screenshot 2026-03-09 at 08 17 07 Screenshot 2026-03-09 at 08 21 04 --------- Co-authored-by: Claude (Opus 4.6) Co-authored-by: Lunny Xiao Co-authored-by: Giteabot --- web_src/css/base.css | 6 +-- web_src/css/markup/content.css | 48 +++-------------- web_src/css/modules/checkbox.css | 67 +++++++++++++++++++++++- web_src/css/modules/dropdown.css | 1 - web_src/css/modules/form.css | 22 ++------ web_src/css/themes/theme-gitea-dark.css | 3 +- web_src/css/themes/theme-gitea-light.css | 3 +- 7 files changed, 84 insertions(+), 66 deletions(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index 0d6c4a2f75..2c7bd7395a 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -15,8 +15,8 @@ /* line-height: use the default value as "modules/normalize.css" */ --line-height-default: normal; /* images */ - --checkbox-mask-checked: url('data:image/svg+xml;utf8,'); - --checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,'); + --checkbox-mask-checked: url('data:image/svg+xml;utf8,'); + --checkbox-mask-indeterminate: url('data:image/svg+xml;utf8,'); --octicon-chevron-right: url('data:image/svg+xml;utf8,'); --select-arrows: url('data:image/svg+xml;utf8,'); /* other variables */ @@ -27,7 +27,7 @@ --height-loading: 16rem; --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */ --tab-size: 4; - --checkbox-size: 15px; /* height and width of checkbox and radio inputs */ + --checkbox-size: 14px; /* height and width of checkbox and radio inputs */ --page-spacing: 16px; /* space between page elements */ --page-margin-x: 32px; /* minimum space on left and right side of page */ --page-space-bottom: 64px; /* space between last page element and footer */ diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 047b03fa19..6ca6f95c69 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -164,12 +164,16 @@ In markup content, we always use bottom margin for all elements */ list-style-type: none; } +.markup .task-list-item > ul { + margin-top: 4px; +} + .markup .task-list-item p + ul { margin-top: 16px; } .markup .task-list-item input[type="checkbox"] { - margin: 0 .3em .25em -1.4em; + margin: 0 .6em .25em -1.4em; vertical-align: middle; padding: 0; } @@ -188,48 +192,12 @@ In markup content, we always use bottom margin for all elements */ } .markup input[type="checkbox"] { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - position: relative; - border: 1px solid var(--color-secondary); - border-radius: var(--border-radius); - background: var(--color-input-background); - height: 14px; - width: 14px; + margin-right: .25em; + margin-bottom: .25em; + cursor: default; opacity: 1 !important; /* override fomantic on edit preview */ pointer-events: auto !important; /* override fomantic on edit preview */ vertical-align: middle !important; /* override fomantic on edit preview */ - -webkit-print-color-adjust: exact; - color-adjust: exact; -} - -.markup input[type="checkbox"]:not([disabled]):hover, -.markup input[type="checkbox"]:not([disabled]):active { - border-color: var(--color-primary); -} - -.markup input[type="checkbox"]::after { - position: absolute; - inset: 0; - pointer-events: none; - background: var(--color-text); - mask-size: cover; - -webkit-mask-size: cover; -} - -.markup input[type="checkbox"]:checked::after { - content: ""; - mask-image: var(--checkbox-mask-checked); - -webkit-mask-image: var(--checkbox-mask-checked); - -webkit-print-color-adjust: exact; - color-adjust: exact; -} - -.markup input[type="checkbox"]:indeterminate::after { - content: ""; - mask-image: var(--checkbox-mask-indeterminate); - -webkit-mask-image: var(--checkbox-mask-indeterminate); } .markup ul ul, diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css index f7e61ba360..558486e63a 100644 --- a/web_src/css/modules/checkbox.css +++ b/web_src/css/modules/checkbox.css @@ -1,10 +1,75 @@ /* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any unused rules here after refactoring, please remove them. */ -input[type="checkbox"], input[type="radio"] { + appearance: none; width: var(--checkbox-size); height: var(--checkbox-size); + border: 1px solid var(--color-input-border); + border-radius: 50%; + background: var(--color-input-background); +} + +input[type="radio"]:checked { + background: var(--color-white); + border: 4px solid var(--color-primary); +} + +input[type="checkbox"] { + appearance: none; + display: inline-grid; + place-content: center; + width: var(--checkbox-size); + height: var(--checkbox-size); + border: 1px solid var(--color-input-border); + border-radius: 3px; + background: var(--color-input-background); + overflow: hidden; + print-color-adjust: exact; +} + +input[type="checkbox"]::before { + content: ""; + background: var(--color-white); + width: var(--checkbox-size); + height: var(--checkbox-size); + clip-path: inset(var(--checkbox-size) 0 0 0); + mask-image: var(--checkbox-mask-checked); + -webkit-mask-image: var(--checkbox-mask-checked); + mask-size: 75%; + -webkit-mask-size: 75%; + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-position: center; +} + +input[type="checkbox"]:checked, +input[type="checkbox"]:indeterminate { + background: var(--color-primary); + border-color: var(--color-primary); +} + +input[type="checkbox"]:checked::before { + clip-path: inset(0); +} + +input[type="checkbox"]:disabled:checked, +input[type="checkbox"]:disabled:indeterminate { + background: var(--color-secondary-dark-4); + border-color: var(--color-secondary-dark-4); +} + +input[type="radio"]:disabled:checked { + border-color: var(--color-secondary-dark-4); +} + +input[type="checkbox"]:indeterminate::before { + clip-path: inset(0); + mask-image: var(--checkbox-mask-indeterminate); + -webkit-mask-image: var(--checkbox-mask-indeterminate); + mask-size: 75%; + -webkit-mask-size: 75%; } .ui.checkbox { diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css index 2008ece2ed..1c6e7f8552 100644 --- a/web_src/css/modules/dropdown.css +++ b/web_src/css/modules/dropdown.css @@ -268,7 +268,6 @@ select.ui.dropdown { } .ui.selection.dropdown:hover { - border-color: var(--color-input-border-hover); box-shadow: none; } diff --git a/web_src/css/modules/form.css b/web_src/css/modules/form.css index 56d4f9ba61..2d315786c6 100644 --- a/web_src/css/modules/form.css +++ b/web_src/css/modules/form.css @@ -43,7 +43,7 @@ height: 1.21428571em; } -.ui.form input, +.ui.form input:not([type="checkbox"], [type="radio"]), .ui.search > .prompt { font-family: var(--fonts-regular); margin: 0; @@ -74,10 +74,10 @@ max-height: 24em; } -input, +input:not([type="checkbox"], [type="radio"]), textarea, .ui.input > input, -.ui.form input, +.ui.form input:not([type="checkbox"], [type="radio"]), .ui.form select, .ui.form textarea, .ui.selection.dropdown, @@ -87,22 +87,10 @@ textarea, color: var(--color-input-text); } -input:hover, -textarea:hover, -.ui.input input:hover, -.ui.form input:hover, -.ui.form select:hover, -.ui.form textarea:hover, -.ui.search > .prompt:hover { - background: var(--color-input-background); - border: 1px solid var(--color-input-border-hover); - color: var(--color-input-text); -} - -input:focus, +input:not([type="checkbox"], [type="radio"]):focus, textarea:focus, .ui.input input:focus, -.ui.form input:focus, +.ui.form input:not([type="checkbox"], [type="radio"]):focus, .ui.form select:focus, .ui.form textarea:focus, .ui.search > .prompt:focus { diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index ad5eec9e82..f347589509 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -205,8 +205,7 @@ gitea-theme-meta-info { --color-input-text: var(--color-text-dark); --color-input-background: #171a1e; --color-input-toggle-background: #2e353c; - --color-input-border: var(--color-secondary); - --color-input-border-hover: var(--color-secondary-dark-1); + --color-input-border: var(--color-secondary-dark-1); --color-light: #00001728; --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #e8f3ff28; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 049b64f73f..dc916f002d 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -205,8 +205,7 @@ gitea-theme-meta-info { --color-input-text: var(--color-text-dark); --color-input-background: #fff; --color-input-toggle-background: #d0d7de; - --color-input-border: var(--color-secondary); - --color-input-border-hover: var(--color-secondary-dark-1); + --color-input-border: var(--color-secondary-dark-1); --color-light: #00001706; --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #0000171d; From 4f9f0fc4b86f1995d3d5ac88b98b04df94394672 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Mar 2026 02:23:42 +0800 Subject: [PATCH 105/207] Fix various trivial problems (#36953) 1. remove `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY` * it defaults to false and is unlikely to be useful for most users (see #22130) * with new git versions (>= 2.40), "merge-tree" is used, "checkConflictsByTmpRepo" isn't called, the option does nothing. 2. fix fragile `db.Cell2Int64` (new: `CellToInt`) 3. allow more routes in maintenance mode (e.g.: captcha) 4. fix MockLocale html escaping to make it have the same behavior as production locale --- custom/conf/app.example.ini | 3 - models/auth/source.go | 6 +- models/auth/source_test.go | 40 +++---- models/db/convert.go | 22 ++-- models/repo/repo_unit.go | 6 +- modules/setting/repository.go | 2 - modules/translation/mock.go | 41 ++++--- routers/common/maintenancemode.go | 48 ++++---- services/pull/patch.go | 162 +-------------------------- tests/integration/pull_merge_test.go | 81 -------------- 10 files changed, 97 insertions(+), 314 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 803231ff12..5eb4a5e995 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1153,9 +1153,6 @@ LEVEL = Info ;; Add co-authored-by and co-committed-by trailers if committer does not match author ;ADD_CO_COMMITTER_TRAILERS = true ;; -;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply -;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false -;; ;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo. ;RETARGET_CHILDREN_ON_MERGE = true ;; diff --git a/models/auth/source.go b/models/auth/source.go index c0b262f870..7a008f08a8 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -139,7 +140,10 @@ func init() { // BeforeSet is invoked from XORM before setting the value of a field of this object. func (source *Source) BeforeSet(colName string, val xorm.Cell) { if colName == "type" { - typ := Type(db.Cell2Int64(val)) + typ, _, err := db.CellToInt(val, NoType) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert login source (id=%d) type: %v", source.ID, err) + } constructor, ok := registeredConfigs[typ] if !ok { return diff --git a/models/auth/source_test.go b/models/auth/source_test.go index ebc462c581..fbd663a64f 100644 --- a/models/auth/source_test.go +++ b/models/auth/source_test.go @@ -17,13 +17,9 @@ import ( ) type TestSource struct { - auth_model.ConfigBase + auth_model.ConfigBase `json:"-"` - Provider string - ClientID string - ClientSecret string - OpenIDConnectAutoDiscoveryURL string - IconURL string + TestField string } // FromDB fills up a LDAPConfig from serialized format. @@ -37,27 +33,23 @@ func (source *TestSource) ToDB() ([]byte, error) { } func TestDumpAuthSource(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + require.NoError(t, unittest.PrepareTestDatabase()) authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source)) - assert.NoError(t, err) + require.NoError(t, err) auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource)) + source := &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "TestSource", + Cfg: &TestSource{TestField: "TestValue"}, + } + require.NoError(t, auth_model.CreateSource(t.Context(), source)) - auth_model.CreateSource(t.Context(), &auth_model.Source{ - Type: auth_model.OAuth2, - Name: "TestSource", - IsActive: false, - Cfg: &TestSource{ - Provider: "ConvertibleSourceName", - ClientID: "42", - }, - }) - - sb := new(strings.Builder) - - // TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump - engine := unittest.GetXORMEngine() - require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb)) - assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`) + // intentionally test the "dump" to make sure the dumped JSON is correct: https://github.com/go-gitea/gitea/pull/16847 + sb := &strings.Builder{} + require.NoError(t, unittest.GetXORMEngine().DumpTables([]*schemas.Table{authSourceSchema}, sb)) + // the dumped SQL is something like: + // INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784); + assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`) } diff --git a/models/db/convert.go b/models/db/convert.go index 80b0f7b04b..374dbfd8a8 100644 --- a/models/db/convert.go +++ b/models/db/convert.go @@ -5,12 +5,11 @@ package db import ( "fmt" - "strconv" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" + "xorm.io/xorm/convert" "xorm.io/xorm/schemas" ) @@ -74,15 +73,14 @@ WHERE ST.name ='varchar'`) return err } -// Cell2Int64 converts a xorm.Cell type to int64, -// and handles possible irregular cases. -func Cell2Int64(val xorm.Cell) int64 { - switch (*val).(type) { - case []uint8: - log.Trace("Cell2Int64 ([]uint8): %v", *val) - - v, _ := strconv.ParseInt(string((*val).([]uint8)), 10, 64) - return v +// CellToInt converts a xorm.Cell field value to an int value +func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err error) { + if *cell == nil { + return def, false, nil } - return (*val).(int64) + val, err := convert.AsInt64(*cell) + if err != nil { + return def, false, err + } + return T(val), true, err } diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 18ebd6be54..c32adbbcd4 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -227,7 +227,11 @@ func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool { func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { case "type": - r.Type = unit.Type(db.Cell2Int64(val)) + var err error + r.Type, _, err = db.CellToInt(val, unit.TypeInvalid) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err) + } switch r.Type { case unit.TypeExternalWiki: r.Config = new(ExternalWikiConfig) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index f4e45d4702..9195b7ee50 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -86,7 +86,6 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int DefaultDeleteBranchAfterMerge bool @@ -211,7 +210,6 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int DefaultDeleteBranchAfterMerge bool diff --git a/modules/translation/mock.go b/modules/translation/mock.go index f457271ea5..02b19d9583 100644 --- a/modules/translation/mock.go +++ b/modules/translation/mock.go @@ -5,8 +5,8 @@ package translation import ( "fmt" + "html" "html/template" - "strings" ) // MockLocale provides a mocked locale without any translations @@ -20,25 +20,40 @@ func (l MockLocale) Language() string { return "en" } -func (l MockLocale) TrString(s string, args ...any) string { - return sprintAny(s, args...) +func (l MockLocale) TrString(format string, args ...any) (ret string) { + ret = format + ":" + for _, arg := range args { + // usually there is no arg or at most 1-2 args, so a simple string concatenation is more efficient + switch v := arg.(type) { + case string: + ret += v + "," + default: + ret += fmt.Sprint(v) + "," + } + } + return ret[:len(ret)-1] } -func (l MockLocale) Tr(s string, args ...any) template.HTML { - return template.HTML(sprintAny(s, args...)) +func (l MockLocale) Tr(format string, args ...any) (ret template.HTML) { + ret = template.HTML(html.EscapeString(format)) + ":" + for _, arg := range args { + // usually there is no arg or at most 1-2 args, so a simple string concatenation is more efficient + switch v := arg.(type) { + case template.HTML: + ret += v + "," + case string: + ret += template.HTML(html.EscapeString(v)) + "," + default: + ret += template.HTML(html.EscapeString(fmt.Sprint(v))) + "," + } + } + return ret[:len(ret)-1] } func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { - return template.HTML(sprintAny(key1, args...)) + return l.Tr(key1, args...) } func (l MockLocale) PrettyNumber(v any) string { return fmt.Sprint(v) } - -func sprintAny(s string, args ...any) string { - if len(args) == 0 { - return s - } - return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...) -} diff --git a/routers/common/maintenancemode.go b/routers/common/maintenancemode.go index b5827ac94f..adf099df57 100644 --- a/routers/common/maintenancemode.go +++ b/routers/common/maintenancemode.go @@ -7,29 +7,39 @@ import ( "net/http" "strings" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/setting" ) -func isMaintenanceModeAllowedRequest(req *http.Request) bool { - if strings.HasPrefix(req.URL.Path, "/-/") { - // URLs like "/-/admin", "/-/fetch-redirect" and "/-/markup" are still accessible in maintenance mode - return true - } - if strings.HasPrefix(req.URL.Path, "/api/internal/") { - // internal APIs should be allowed - return true - } - if strings.HasPrefix(req.URL.Path, "/user/") { - // URLs like "/user/signin" and "/user/signup" are still accessible in maintenance mode - return true - } - if strings.HasPrefix(req.URL.Path, "/assets/") { - return true - } - return false -} - func MaintenanceModeHandler() func(h http.Handler) http.Handler { + allowedPrefixes := []string{ + "/.well-known/", + "/assets/", + "/avatars/", + + // admin: "/-/admin" + // general-purpose URLs: "/-/fetch-redirect", "/-/markup", etc. + "/-/", + + // internal APIs + "/api/internal/", + + // user login (for admin to login): "/user/login", "/user/logout", "/catpcha/..." + "/user/", + "/captcha/", + } + allowedPaths := container.SetOf( + "/api/healthz", + ) + isMaintenanceModeAllowedRequest := func(req *http.Request) bool { + for _, prefix := range allowedPrefixes { + if strings.HasPrefix(req.URL.Path, prefix) { + return true + } + } + return allowedPaths.Contains(req.URL.Path) + } + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { maintenanceMode := setting.Config().Instance.MaintenanceMode.Value(req.Context()) diff --git a/services/pull/patch.go b/services/pull/patch.go index 30f07f8931..114a437cbc 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -5,7 +5,6 @@ package pull import ( - "bufio" "context" "fmt" "io" @@ -15,15 +14,12 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -57,15 +53,6 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io return nil } -var patchErrorSuffices = []string{ - ": already exists in index", - ": patch does not apply", - ": already exists in working directory", - "unrecognized input", - ": No such file or directory", - ": does not exist in index", -} - func checkPullRequestBranchMergeable(ctx context.Context, pr *issues_model.PullRequest) error { ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("checkPullRequestBranchMergeable: %s", pr)) defer finished() @@ -349,151 +336,10 @@ func checkConflictsByTmpRepo(ctx context.Context, pr *issues_model.PullRequest, } // 3. OK the three-way merge method has detected conflicts - // 3a. Are still testing with GitApply? If not set the conflict status and move on - if !setting.Repository.PullRequest.TestConflictingPatchesWithGitApply { - pr.Status = issues_model.PullRequestStatusConflict - pr.ConflictedFiles = conflictFiles - - log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) - return true, nil - } - - // 3b. Create a plain patch from head to base - tmpPatchFile, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("patch") - if err != nil { - log.Error("Unable to create temporary patch file! Error: %v", err) - return false, fmt.Errorf("unable to create temporary patch file! Error: %w", err) - } - defer cleanup() - - if err := gitRepo.GetDiffBinary(pr.MergeBase+"...tracking", tmpPatchFile); err != nil { - log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) - return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) - } - stat, err := tmpPatchFile.Stat() - if err != nil { - return false, fmt.Errorf("unable to stat patch file: %w", err) - } - patchPath := tmpPatchFile.Name() - tmpPatchFile.Close() - - // 3c. if the size of that patch is 0 - there can be no conflicts! - if stat.Size() == 0 { - log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) - pr.Status = issues_model.PullRequestStatusEmpty - return false, nil - } - - log.Trace("PullRequest[%d].checkPullRequestMergeableByTmpRepo (patchPath): %s", pr.ID, patchPath) - - // 4. Read the base branch in to the index of the temporary repository - _, _, err = gitcmd.NewCommand("read-tree", tmpRepoBaseBranch).WithDir(tmpBasePath).RunStdString(ctx) - if err != nil { - return false, fmt.Errorf("git read-tree %s: %w", pr.BaseBranch, err) - } - - // 5. Now get the pull request configuration to check if we need to ignore whitespace - prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) - if err != nil { - return false, err - } - prConfig := prUnit.PullRequestsConfig() - - // 6. Prepare the arguments to apply the patch against the index - cmdApply := gitcmd.NewCommand("apply", "--check", "--cached") - if prConfig.IgnoreWhitespaceConflicts { - cmdApply.AddArguments("--ignore-whitespace") - } - is3way := false - if git.DefaultFeatures().CheckVersionAtLeast("2.32.0") { - cmdApply.AddArguments("--3way") - is3way = true - } - cmdApply.AddDynamicArguments(patchPath) - - // 7. Prep the pipe: - // - Here we could do the equivalent of: - // `git apply --check --cached patch_file > conflicts` - // Then iterate through the conflicts. However, that means storing all the conflicts - // in memory - which is very wasteful. - // - alternatively we can do the equivalent of: - // `git apply --check ... | grep ...` - // meaning we don't store all the conflicts unnecessarily. - stderrReader, stderrReaderClose := cmdApply.MakeStderrPipe() - defer stderrReaderClose() - - // 8. Run the check command - conflict = false - err = cmdApply. - WithDir(tmpBasePath). - WithPipelineFunc(func(ctx gitcmd.Context) error { - const prefix = "error: patch failed:" - const errorPrefix = "error: " - const threewayFailed = "Failed to perform three-way merge..." - const appliedPatchPrefix = "Applied patch to '" - const withConflicts = "' with conflicts." - - conflicts := make(container.Set[string]) - - // Now scan the output from the command - scanner := bufio.NewScanner(stderrReader) - for scanner.Scan() { - line := scanner.Text() - log.Trace("PullRequest[%d].checkPullRequestMergeableByTmpRepo: stderr: %s", pr.ID, line) - if strings.HasPrefix(line, prefix) { - conflict = true - filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]) - conflicts.Add(filepath) - } else if is3way && line == threewayFailed { - conflict = true - } else if strings.HasPrefix(line, errorPrefix) { - conflict = true - for _, suffix := range patchErrorSuffices { - if strings.HasSuffix(line, suffix) { - filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix)) - if filepath != "" { - conflicts.Add(filepath) - } - break - } - } - } else if is3way && strings.HasPrefix(line, appliedPatchPrefix) && strings.HasSuffix(line, withConflicts) { - conflict = true - filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix) - if filepath != "" { - conflicts.Add(filepath) - } - } - // only list part of conflicted files - if len(conflicts) >= gitrepo.MaxConflictedDetectFiles { - break - } - } - - if len(conflicts) > 0 { - pr.ConflictedFiles = make([]string, 0, len(conflicts)) - for key := range conflicts { - pr.ConflictedFiles = append(pr.ConflictedFiles, key) - } - } - - return nil - }). - Run(gitRepo.Ctx) - - // 9. Check if the found conflicted files is non-zero, "err" could be non-nil, so we should ignore it if we found conflicts. - // Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts. - if len(pr.ConflictedFiles) > 0 { - if conflict { - pr.Status = issues_model.PullRequestStatusConflict - log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) - - return true, nil - } - } else if err != nil { - return false, fmt.Errorf("git apply --check: %w", err) - } - return false, nil + pr.Status = issues_model.PullRequestStatusConflict + pr.ConflictedFiles = conflictFiles + log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) + return true, nil } // ErrFilePathProtected represents a "FilePathProtected" kind of error. diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index c2859e2e16..53709e6ff4 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -39,7 +39,6 @@ import ( pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" - files_service "code.gitea.io/gitea/services/repository/files" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -549,86 +548,6 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { }) } -func TestConflictChecking(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - // Create new clean repo to test conflict checking. - baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{ - Name: "conflict-checking", - Description: "Tempo repo", - AutoInit: true, - Readme: "Default", - DefaultBranch: "main", - }) - assert.NoError(t, err) - assert.NotEmpty(t, baseRepo) - - // create a commit on new branch. - _, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "create", - TreePath: "important_file", - ContentReader: strings.NewReader("Just a non-important file"), - }, - }, - Message: "Add a important file", - OldBranch: "main", - NewBranch: "important-secrets", - }) - assert.NoError(t, err) - - // create a commit on main branch. - _, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "create", - TreePath: "important_file", - ContentReader: strings.NewReader("Not the same content :P"), - }, - }, - Message: "Add a important file", - OldBranch: "main", - NewBranch: "main", - }) - assert.NoError(t, err) - - // create Pull to merge the important-secrets branch into main branch. - pullIssue := &issues_model.Issue{ - RepoID: baseRepo.ID, - Title: "PR with conflict!", - PosterID: user.ID, - Poster: user, - IsPull: true, - } - - pullRequest := &issues_model.PullRequest{ - HeadRepoID: baseRepo.ID, - BaseRepoID: baseRepo.ID, - HeadBranch: "important-secrets", - BaseBranch: "main", - HeadRepo: baseRepo, - BaseRepo: baseRepo, - Type: issues_model.PullRequestGitea, - } - prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest} - err = pull_service.NewPullRequest(t.Context(), prOpts) - assert.NoError(t, err) - - issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"}) - assert.NoError(t, issue.LoadPullRequest(t.Context())) - conflictingPR := issue.PullRequest - - // Ensure conflictedFiles is populated. - assert.Len(t, conflictingPR.ConflictedFiles, 1) - // Check if status is correct. - assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status) - // Ensure that mergeable returns false - assert.False(t, conflictingPR.Mergeable(t.Context())) - }) -} - func TestPullRetargetChildOnBranchDelete(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") // FIXME: don't use admin user for testing From cf1e4d7c42ac530162eeea7b8decf94d235f0d97 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 22:42:36 +0100 Subject: [PATCH 106/207] Update GitHub Actions to latest major versions (#36964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update all Actions to their latest major versions: - `actions/checkout`: v5 → v6 - `dorny/paths-filter`: v3 → v4 - `pnpm/action-setup`: v4 → v5 - `docker/setup-qemu-action`: v3 → v4 - `docker/setup-buildx-action`: v3 → v4 - `docker/build-push-action`: v6 → v7 - `docker/metadata-action`: v5 → v6 - `docker/login-action`: v3 → v4 - `crazy-max/ghaction-import-gpg`: v6 → v7 - `aws-actions/configure-aws-credentials`: v5 → v6 All updates are Node 24 runtime bumps with no workflow-breaking changes for our usage. Co-authored-by: Claude (Opus 4.6) --- .github/workflows/cron-flake-updater.yml | 2 +- .github/workflows/files-changed.yml | 2 +- .github/workflows/pull-compliance.yml | 10 +++++----- .github/workflows/pull-docker-dryrun.yml | 8 ++++---- .github/workflows/pull-e2e-tests.yml | 2 +- .github/workflows/release-nightly.yml | 22 +++++++++++----------- .github/workflows/release-tag-rc.yml | 22 +++++++++++----------- .github/workflows/release-tag-version.yml | 22 +++++++++++----------- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.github/workflows/cron-flake-updater.yml b/.github/workflows/cron-flake-updater.yml index 105802e558..c9a1f22a2a 100644 --- a/.github/workflows/cron-flake-updater.yml +++ b/.github/workflows/cron-flake-updater.yml @@ -13,7 +13,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: DeterminateSystems/determinate-nix-action@v3 - uses: DeterminateSystems/update-flake-lock@main with: diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 332e9e0d6f..55d206bb0f 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -40,7 +40,7 @@ jobs: json: ${{ steps.changes.outputs.json }} steps: - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@v4 id: changes with: filters: | diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index fb81622bd6..e44a787587 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 - run: uv python install 3.14 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -71,7 +71,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v5 with: node-version: 24 @@ -86,7 +86,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -168,7 +168,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -222,7 +222,7 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index bcc19e3eba..201825ccba 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -21,17 +21,17 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 - name: Build regular container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 push: false cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful - name: Build rootless container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: false diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index c77f7af3f0..3472d517c1 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -25,7 +25,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index a7b2fda042..eaebccd7fb 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -22,7 +22,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -35,7 +35,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -52,7 +52,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -71,14 +71,14 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 - name: Get cleaned branch name id: clean_name run: | REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -88,7 +88,7 @@ jobs: type=raw,value=${{ steps.clean_name.outputs.branch }} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta_rootless with: images: |- @@ -102,18 +102,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -123,7 +123,7 @@ jobs: cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max - name: build rootless docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index fab468c9b4..248fa532ee 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -23,7 +23,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -36,7 +36,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -53,7 +53,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -81,9 +81,9 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -96,7 +96,7 @@ jobs: type=semver,pattern={{version}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta_rootless with: images: |- @@ -112,18 +112,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -131,7 +131,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 113a33c3c7..1e84ae1739 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -26,7 +26,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: node-version: 24 @@ -39,7 +39,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -56,7 +56,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -84,9 +84,9 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -103,7 +103,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@v5 + - uses: docker/metadata-action@v6 id: meta_rootless with: images: |- @@ -124,18 +124,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -143,7 +143,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 From 86401fd5fd35fc8f337dfc052cb63e53d9c2017a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 23 Mar 2026 23:30:48 +0100 Subject: [PATCH 107/207] Fix user settings sidebar showing disabled features on some pages (#36958) Move UserDisabledFeatures context data into a shared SettingsCtxData middleware for the /user/settings route group, so it is set consistently on all pages (including Notifications, Actions, etc.) instead of only on the handlers that remembered to set it individually. Fixes #36954 --- routers/web/repo/setting/secrets.go | 2 -- routers/web/user/setting/account.go | 1 - routers/web/user/setting/applications.go | 3 --- routers/web/user/setting/block.go | 2 -- routers/web/user/setting/keys.go | 3 --- routers/web/user/setting/packages.go | 6 ------ routers/web/user/setting/profile.go | 6 ------ routers/web/user/setting/security/security.go | 1 - routers/web/user/setting/settings.go | 8 ++++++++ routers/web/user/setting/webhooks.go | 2 -- routers/web/web.go | 2 +- 11 files changed, 9 insertions(+), 27 deletions(-) diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index cd32a7dbb7..419aa26867 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -7,7 +7,6 @@ import ( "errors" "net/http" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" shared "code.gitea.io/gitea/routers/web/shared/secrets" @@ -74,7 +73,6 @@ func Secrets(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageType"] = "secrets" ctx.Data["PageIsSharedSettingsSecrets"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) sCtx, err := getSecretsCtx(ctx) if err != nil { diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index b333f36462..23bdf33d5f 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -321,7 +321,6 @@ func loadAccountData(ctx *context.Context) { ctx.Data["Emails"] = emails ctx.Data["ActivationsPending"] = pendingActivation ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if setting.Service.UserDeleteWithCommentsMaxTime != 0 { ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String() diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index 2498c43b84..9e33b487ea 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -10,7 +10,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" @@ -27,7 +26,6 @@ const ( func Applications(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.applications") ctx.Data["PageIsSettingsApplications"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) loadApplicationsData(ctx) @@ -39,7 +37,6 @@ func ApplicationsPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewAccessTokenForm) ctx.Data["Title"] = ctx.Tr("settings_title") ctx.Data["PageIsSettingsApplications"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) _ = ctx.Req.ParseForm() var scopeNames []string diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go index 3756495fd2..3a1625ccf9 100644 --- a/routers/web/user/setting/block.go +++ b/routers/web/user/setting/block.go @@ -6,7 +6,6 @@ package setting import ( "net/http" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" shared_user "code.gitea.io/gitea/routers/web/shared/user" @@ -20,7 +19,6 @@ const ( func BlockedUsers(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("user.block.list") ctx.Data["PageIsSettingsBlockedUsers"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared_user.BlockedUsers(ctx, ctx.Doer) if ctx.Written() { diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index b78a0ec434..b82fa24a5d 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -35,7 +35,6 @@ func Keys(ctx *context.Context) { ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) loadKeysData(ctx) @@ -50,7 +49,6 @@ func KeysPost(ctx *context.Context) { ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer ctx.Data["AllowPrincipals"] = setting.SSH.AuthorizedPrincipalsEnabled - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if ctx.HasError() { loadKeysData(ctx) @@ -341,5 +339,4 @@ func loadKeysData(ctx *context.Context) { ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg") ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh") - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) } diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go index 51f8c46908..62b0240642 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -25,7 +25,6 @@ const ( func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetPackagesContext(ctx, ctx.Doer) @@ -35,7 +34,6 @@ func Packages(ctx *context.Context) { func PackagesRuleAdd(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRuleAddContext(ctx) @@ -45,7 +43,6 @@ func PackagesRuleAdd(ctx *context.Context) { func PackagesRuleEdit(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRuleEditContext(ctx, ctx.Doer) @@ -55,7 +52,6 @@ func PackagesRuleEdit(ctx *context.Context) { func PackagesRuleAddPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings_title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.PerformRuleAddPost( ctx, @@ -68,7 +64,6 @@ func PackagesRuleAddPost(ctx *context.Context) { func PackagesRuleEditPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.PerformRuleEditPost( ctx, @@ -81,7 +76,6 @@ func PackagesRuleEditPost(ctx *context.Context) { func PackagesRulePreview(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["PageIsSettingsPackages"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) shared.SetRulePreviewContext(ctx, ctx.Doer) diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 81a4a558e9..88d8e75d0c 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -49,8 +49,6 @@ func Profile(ctx *context.Context) { ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) - ctx.HTML(http.StatusOK, tplSettingsProfile) } @@ -60,7 +58,6 @@ func ProfilePost(ctx *context.Context) { ctx.Data["PageIsSettingsProfile"] = true ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsProfile) @@ -200,7 +197,6 @@ func DeleteAvatar(ctx *context.Context) { func Organization(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.organization") ctx.Data["PageIsSettingsOrganization"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) opts := organization.FindOrgOptions{ ListOptions: db.ListOptions{ @@ -232,7 +228,6 @@ func Organization(ctx *context.Context) { func Repos(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.repos") ctx.Data["PageIsSettingsRepos"] = true - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories @@ -340,7 +335,6 @@ func Appearance(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.appearance") ctx.Data["PageIsSettingsAppearance"] = true ctx.Data["AllThemes"] = webtheme.GetAvailableThemes() - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) var hiddenCommentTypes *big.Int val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index cc4c44993a..1b7efa27d5 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -156,5 +156,4 @@ func loadSecurityData(ctx *context.Context) { return } ctx.Data["OpenIDs"] = openid - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) } diff --git a/routers/web/user/setting/settings.go b/routers/web/user/setting/settings.go index 111931633d..e02ddc39af 100644 --- a/routers/web/user/setting/settings.go +++ b/routers/web/user/setting/settings.go @@ -9,9 +9,17 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" ) +func SettingsCtxData(ctx *context.Context) { + ctx.Data["PageIsUserSettings"] = true + ctx.Data["EnablePackages"] = setting.Packages.Enabled + ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail + ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) +} + func UpdatePreferences(ctx *context.Context) { type preferencesForm struct { CodeViewShowFileTree bool `json:"codeViewShowFileTree"` diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go index 72a95a92e5..3c5d54cbc6 100644 --- a/routers/web/user/setting/webhooks.go +++ b/routers/web/user/setting/webhooks.go @@ -7,7 +7,6 @@ import ( "net/http" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -25,7 +24,6 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks" ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks" ctx.Data["Description"] = ctx.Tr("settings.hooks.desc") - ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID}) if err != nil { diff --git a/routers/web/web.go b/routers/web/web.go index a76a68ed80..75cc437b43 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -721,7 +721,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("", user_setting.BlockedUsers) m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost) }) - }, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled, "EnableNotifyMail", setting.Service.EnableNotifyMail)) + }, reqSignIn, user_setting.SettingsCtxData) m.Group("/user", func() { m.Get("/activate", auth.Activate) From 63c2b692597a384168f8383a58aa590430b04c92 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Mar 2026 07:19:08 +0800 Subject: [PATCH 108/207] Make PUBLIC_URL_DETECTION default to "auto" (#36955) Related issues including: #36939 , #35619, #34950 , #34253 , #32554 For users who use reverse-proxy, we have documented the requirements clearly since long time ago : https://docs.gitea.com/administration/reverse-proxies --- custom/conf/app.example.ini | 9 ++++----- modules/setting/server.go | 2 +- routers/web/admin/admin_test.go | 23 +++++++++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5eb4a5e995..b752a81ca9 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -69,13 +69,12 @@ RUN_USER = ; git ;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. ;ROOT_URL = ;; -;; Controls how to detect the public URL. -;; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior, +;; Controls how to detect the public URL. Most instances should use the "auto" behavior, ;; especially when the Gitea instance needs to be accessed in a container network. -;; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL". -;; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL". +;; * legacy: (default <= 1.25) detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL". +;; * auto: (default >= 1.26) always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL". ;; * never: always use "ROOT_URL", never detect from request headers. -;PUBLIC_URL_DETECTION = legacy +;PUBLIC_URL_DETECTION = auto ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; DO NOT USE IT IN PRODUCTION!!! diff --git a/modules/setting/server.go b/modules/setting/server.go index 7e7611b802..f0fbbce970 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -286,7 +286,7 @@ func loadServerFrom(rootCfg ConfigProvider) { defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) - PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy) + PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto) if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever { log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection) } diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index a568c7c5c8..ecdd462f9e 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestShadowPassword(t *testing.T) { @@ -74,19 +75,29 @@ func TestShadowPassword(t *testing.T) { } func TestSelfCheckPost(t *testing.T) { + defer test.MockVariableValue(&setting.PublicURLDetection)() defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() - ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") - SelfCheckPost(ctx) - assert.Equal(t, http.StatusOK, resp.Code) - data := struct { Problems []string `json:"problems"` }{} - err := json.Unmarshal(resp.Body.Bytes(), &data) - assert.NoError(t, err) + + setting.PublicURLDetection = setting.PublicURLLegacy + ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") + SelfCheckPost(ctx) + assert.Equal(t, http.StatusOK, resp.Code) + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data)) assert.Equal(t, []string{ ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://config/sub/"), }, data.Problems) + + setting.PublicURLDetection = setting.PublicURLAuto + ctx, resp = contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") + SelfCheckPost(ctx) + assert.Equal(t, http.StatusOK, resp.Code) + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data)) + assert.Equal(t, []string{ + ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"), + }, data.Problems) } From c5e196dedb8a5f145203dd956907726450411d6f Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 24 Mar 2026 00:45:32 +0000 Subject: [PATCH 109/207] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 53 +++++++++++++++++++- options/locale/locale_ko-KR.json | 83 ++++++++++++++++++++++++++------ 2 files changed, 119 insertions(+), 17 deletions(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index f43c9ad355..663de75772 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -81,6 +81,7 @@ "retry": "Atriail", "rerun": "Ath-rith", "rerun_all": "Ath-rith na poist go léir", + "rerun_failed": "Athrith poist theipithe", "save": "Sábháil", "add": "Cuir", "add_all": "Cuir Gach", @@ -168,6 +169,7 @@ "search.exact_tooltip": "Ní chuir san áireamh ach torthaí a mheaitseálann leis an téarma", "search.repo_kind": "Cuardaigh stórais…", "search.user_kind": "Cuardaigh úsáideoirí…", + "search.badge_kind": "Cuardaigh suaitheantais…", "search.org_kind": "Cuardaigh eagraíochtaí…", "search.team_kind": "Cuardaigh foirne…", "search.code_kind": "Cuardaigh cód…", @@ -542,6 +544,7 @@ "form.glob_pattern_error": " tá patrún glob neamhbhailí: %s.", "form.regex_pattern_error": "tá patrún regex neamhbhailí: %s.", "form.username_error": "Ní féidir ach carachtair alfa-uimhriúla ('0-9','a-z','A-Z'), fleasc ('-'), fo-líne ('_') agus ponc ('.') a bheith i `. Ní féidir é a thosú ná a chríochnú le carachtair neamh-alfa-uimhriúla, agus tá cosc ar charachtair neamh-alfa-uimhriúla as a chéile ach an oiread.`", + "form.invalid_slug_error": " neamhbhailí.", "form.invalid_group_team_map_error": " tá mapáil neamhbhailí: %s", "form.unknown_error": "Earráid anaithnid:", "form.captcha_incorrect": "Tá an cód CAPTCHA mícheart.", @@ -645,6 +648,7 @@ "user.block.note.edit": "Cuir nóta in eagar", "user.block.list": "Úsáideoirí blocáilte", "user.block.list.none": "Níor chuir tú bac ar aon úsáideoirí.", + "settings.general": "Ginearálta", "settings.profile": "Próifíl", "settings.account": "Cuntas", "settings.appearance": "Dealramh", @@ -2856,6 +2860,30 @@ "admin.hooks": "Crúcaí Gréasán", "admin.integrations": "Comhtháthaithe", "admin.authentication": "Foinsí Fíordheimhnithe", + "admin.badges": "Suaitheantais", + "admin.badges.badges_manage_panel": "Bainistíocht Suaitheantais", + "admin.badges.details": "Sonraí Suaitheantais", + "admin.badges.new_badge": "Cruthaigh Suaitheantas Nua", + "admin.badges.slug": "Sluga", + "admin.badges.slug_been_taken": "Tá an sluga tógtha cheana féin.", + "admin.badges.description": "Cur síos", + "admin.badges.image_url": "URL na híomhá", + "admin.badges.new_success": "Tá an suaitheantas \"%s\" cruthaithe.", + "admin.badges.update_success": "Tá an suaitheantas nuashonraithe.", + "admin.badges.deletion_success": "Tá an suaitheantas scriosta.", + "admin.badges.edit_badge": "Cuir Suaitheantas in Eagar", + "admin.badges.update_badge": "Nuashonraigh Suaitheantas", + "admin.badges.delete_badge": "Scrios Suaitheantas", + "admin.badges.delete_badge_desc": "An bhfuil tú cinnte gur mian leat an suaitheantas seo a scriosadh go buan?", + "admin.badges.users_with_badge": "Úsáideoirí a bhfuil suaitheantas acu: %s", + "admin.badges.not_found": "Níor aimsíodh an suaitheantas.", + "admin.badges.user_already_has": "Tá an suaitheantas seo ag an úsáideoir cheana féin.", + "admin.badges.user_add_success": "Sannadh suaitheantas don úsáideoir go rathúil.", + "admin.badges.user_remove_success": "Baineadh an suaitheantas den úsáideoir go rathúil.", + "admin.badges.manage_users": "Bainistigh Úsáideoirí", + "admin.badges.add_user": "Cuir Úsáideoir leis", + "admin.badges.remove_user": "Bain Úsáideoir", + "admin.badges.delete_user_desc": "An bhfuil tú cinnte gur mian leat an t-úsáideoir seo a bhaint den suaitheantas?", "admin.emails": "Seoltaí Ríomhphoist Úsáideoirí", "admin.config": "Cumraíocht", "admin.config_summary": "Achoimre", @@ -3707,6 +3735,10 @@ "actions.runs.not_done": "Níl an rith sreabha oibre seo críochnaithe.", "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", "actions.runs.workflow_graph": "Graf Sreabhadh Oibre", + "actions.runs.summary": "Achoimre", + "actions.runs.all_jobs": "Gach post", + "actions.runs.triggered_via": "Spreagtha trí %s", + "actions.runs.total_duration": "Fad iomlán:", "actions.workflow.disable": "Díchumasaigh sreabhadh oibre", "actions.workflow.disable_success": "D'éirigh le sreabhadh oibre '%s' a dhíchumasú.", "actions.workflow.enable": "Cumasaigh sreabhadh oibre", @@ -3756,5 +3788,24 @@ "git.filemode.normal_file": "Rialta", "git.filemode.executable_file": "Inrite", "git.filemode.symbolic_link": "Nasc siombalach", - "git.filemode.submodule": "Fo-mhodúl" + "git.filemode.submodule": "Fo-mhodúl", + "org.repos.none": "Gan aon stórtha.", + "actions.general.permissions": "Ceadanna Comhartha Gníomhartha", + "actions.general.token_permissions.mode": "Ceadanna Réamhshocraithe Comharthaí", + "actions.general.token_permissions.mode.desc": "Úsáidfidh post Gníomhartha na ceadanna réamhshocraithe mura ndearbhaíonn sé a cheadanna sa chomhad sreabha oibre.", + "actions.general.token_permissions.mode.permissive": "Ceadaitheach", + "actions.general.token_permissions.mode.permissive.desc": "Ceadanna léigh agus scríbhneoireachta do stórlann an phoist.", + "actions.general.token_permissions.mode.restricted": "Srianta", + "actions.general.token_permissions.mode.restricted.desc": "Ceadanna léite amháin d'aonaid ábhair (cód, eisiúintí) stórlann an phoist.", + "actions.general.token_permissions.override_owner": "Sáraigh cumraíocht leibhéal an úinéara", + "actions.general.token_permissions.override_owner_desc": "Má tá sé cumasaithe, úsáidfidh an stór seo a chumraíocht Gníomhartha féin in ionad an chumraíocht ar leibhéal an úinéara (úsáideoir nó eagraíocht) a leanúint.", + "actions.general.token_permissions.maximum": "Uasmhéid Ceadanna Comharthaí", + "actions.general.token_permissions.maximum.description": "Beidh ceadanna éifeachtacha an phoist gníomhartha teoranta ag na ceadanna uasta.", + "actions.general.token_permissions.fork_pr_note": "Mura gcuirtear tús le post trí iarratas tarraingthe ó fhorc, ní rachaidh a cheadanna éifeachtacha thar na ceadanna léite amháin.", + "actions.general.token_permissions.customize_max_permissions": "Saincheap na ceadanna uasta", + "actions.general.cross_repo": "Rochtain Tras-Stórtha", + "actions.general.cross_repo_desc": "Ceadaigh rochtain (léamh amháin) a bheith ag na stórtha uile san úinéir seo ar na stórtha roghnaithe le GITEA_TOKEN agus poist Gníomhartha á reáchtáil.", + "actions.general.cross_repo_selected": "Stórtha roghnaithe", + "actions.general.cross_repo_target_repos": "Stórtha Spriocdhírithe", + "actions.general.cross_repo_add": "Cuir Stór Sprioc leis" } diff --git a/options/locale/locale_ko-KR.json b/options/locale/locale_ko-KR.json index 8f572e9f9b..e4ea00f31c 100644 --- a/options/locale/locale_ko-KR.json +++ b/options/locale/locale_ko-KR.json @@ -81,6 +81,7 @@ "retry": "재시도", "rerun": "다시 실행", "rerun_all": "모든 작업 다시 실행", + "rerun_failed": "실패한 작업 다시 실행", "save": "저장", "add": "추가", "add_all": "모두 추가", @@ -168,6 +169,7 @@ "search.exact_tooltip": "검색어와 정확하게 일치하는 결과만 포함합니다", "search.repo_kind": "리포지토리 검색…", "search.user_kind": "사용자 검색…", + "search.badge_kind": "배지 검색…", "search.org_kind": "조직 검색…", "search.team_kind": "팀 검색…", "search.code_kind": "코드 검색…", @@ -542,6 +544,7 @@ "form.glob_pattern_error": " 와일드카드 패턴이 잘못됨: %s.", "form.regex_pattern_error": " 정규 표현식 패턴이 잘못됨: %s.", "form.username_error": " `영숫자 ('0-9','a-z','A-Z'), 대시('-'), 밑줄('_'), 마침점 ('.') 만 포함할 수 있습니다. 시작과 끝문자는 반드시 영숫자이어야 하고, 영숫자가 아닌 문자를 연속해서 사용할 수 없습니다.`.", + "form.invalid_slug_error": " 유효하지 않음.", "form.invalid_group_team_map_error": " 매핑이 잘못되었습니다: %s", "form.unknown_error": "알 수 없는 오류:", "form.captcha_incorrect": "CAPTCHA 코드가 올바르지 않습니다.", @@ -645,6 +648,7 @@ "user.block.note.edit": "노트 편집", "user.block.list": "차단된 사용자", "user.block.list.none": "차단한 사용자가 없습니다.", + "settings.general": "일반", "settings.profile": "프로필", "settings.account": "계정", "settings.appearance": "외관", @@ -1154,7 +1158,7 @@ "repo.unstar": "별점취소", "repo.star": "별점", "repo.fork": "포크", - "repo.action.blocked_user": "리포지토리 소유자에게 차단되어 작업을 수행할 수 없습니다.", + "repo.action.blocked_user": "리포지토리 소유자에게 차단되어 액션을 수행할 수 없습니다.", "repo.download_archive": "리포지토리 다운로드", "repo.more_operations": "추가 작업", "repo.quick_guide": "퀵 가이드", @@ -1178,7 +1182,7 @@ "repo.pulls": "풀 리퀘스트", "repo.projects": "프로젝트", "repo.packages": "패키지", - "repo.actions": "동작", + "repo.actions": "액션", "repo.labels": "레이블", "repo.org_labels_desc": "이 조직의 모든 리포지토리에서 사용할 수 있는 조직 수준 레이블", "repo.org_labels_desc_manage": "관리", @@ -2140,7 +2144,7 @@ "repo.settings.projects_mode_repo": "리포지토리 프로젝트만", "repo.settings.projects_mode_owner": "사용자 또는 조직 프로젝트만", "repo.settings.projects_mode_all": "모든 프로젝트", - "repo.settings.actions_desc": "리포지토리 동작 활성화", + "repo.settings.actions_desc": "리포지토리 액션 활성화", "repo.settings.admin_settings": "운영자 설정", "repo.settings.admin_enable_health_check": "리포지토리 헬스 체크 활성화 (git fsck)", "repo.settings.admin_code_indexer": "코드 인덱서", @@ -2174,7 +2178,7 @@ "repo.settings.transfer_in_progress": "현재 진행 중인 이전이 있습니다. 이 저장소를 다른 사용자에게 이전하려면 먼저 취소하십시오.", "repo.settings.transfer_notices_1": "- 개별 사용자에게 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 잃게 됩니다.", "repo.settings.transfer_notices_2": "- 당신이 (공동)소유하고 있는 조직으로 리포지토리를 이전하면 리포지토리에 대한 액세스 권한을 유지합니다.", - "repo.settings.transfer_notices_3": "- 리포지토리가 비공개이고 개별 사용자에게 이전되는 경우, 이 작업은 해당 사용자가 최소한 읽기 권한을 가지도록 보장합니다 (필요하다면 권한을 변경함).", + "repo.settings.transfer_notices_3": "- 저장소가 비공개이고 개별 사용자에게 이전되는 경우, 이 액션은 해당 사용자가 최소한 읽기 권한을 가지도록 보장합니다(필요하다면 권한을 변경함).", "repo.settings.transfer_notices_4": "- 리포지토리가 조직에 속하고 다른 조직 또는 개인에게 이전하는 경우, 저장소의 이슈와 조직의 프로젝트 보드 간의 연결이 끊어집니다.", "repo.settings.transfer_owner": "새 소유자", "repo.settings.transfer_perform": "이전 수행", @@ -2789,7 +2793,7 @@ "org.teams.can_create_org_repo": "리포지토리 생성", "org.teams.can_create_org_repo_helper": "멤버는 조직에서 새 리포지토리를 만들 수 있습니다. 생성자는 새 리포지토리에 대한 운영자 액세스 권한을 얻습니다.", "org.teams.none_access": "액세스 없음", - "org.teams.none_access_helper": "멤버는 이 단위에서 어떤 작업도 보거나 수행할 수 없습니다. 공개 리포지토리에는 영향을 미치지 않습니다.", + "org.teams.none_access_helper": "멤버는 이 단위에서 어떤 액션도 보거나 수행할 수 없습니다. 공개 리포지토리에는 영향을 미치지 않습니다.", "org.teams.general_access": "일반 액세스", "org.teams.general_access_helper": "멤버 권한은 아래 권한 테이블에 따라 결정됩니다.", "org.teams.read_access": "읽음", @@ -2856,6 +2860,30 @@ "admin.hooks": "Webhook", "admin.integrations": "통합", "admin.authentication": "인증 소스", + "admin.badges": "배지", + "admin.badges.badges_manage_panel": "배지 관리", + "admin.badges.details": "배지 상세정보", + "admin.badges.new_badge": "새 배지 만들기", + "admin.badges.slug": "슬러그", + "admin.badges.slug_been_taken": "이미 사용 중인 슬러그입니다.", + "admin.badges.description": "설명", + "admin.badges.image_url": "이미지 URL", + "admin.badges.new_success": "배지 \"%s\"가 생성되었습니다.", + "admin.badges.update_success": "배지가 업데이트 되었습니다.", + "admin.badges.deletion_success": "배지가 삭제되었습니다.", + "admin.badges.edit_badge": "배지 수정", + "admin.badges.update_badge": "배지 업데이트", + "admin.badges.delete_badge": "배지 삭제", + "admin.badges.delete_badge_desc": "이 배지를 영구적으로 삭제하겠습니까?", + "admin.badges.users_with_badge": "배지를 가진 사용자: %s", + "admin.badges.not_found": "배지를 찾을 수 없음.", + "admin.badges.user_already_has": "사용자는 이 배지를 이미 갖고 있습니다.", + "admin.badges.user_add_success": "사용자에게 배지가 성공적으로 지정되었습니다.", + "admin.badges.user_remove_success": "사용자에게 배지를 제거하는데 성공하였습니다.", + "admin.badges.manage_users": "사용자 관리", + "admin.badges.add_user": "사용자 추가", + "admin.badges.remove_user": "사용자 삭제", + "admin.badges.delete_user_desc": "이 사용자를 배지에서 제거하는 것이 확실한가요?", "admin.emails": "사용자 이메일 주소", "admin.config": "구성", "admin.config_summary": "요약", @@ -2909,7 +2937,7 @@ "admin.dashboard.sync_external_users": "외부 사용자 데이터 동기화", "admin.dashboard.cleanup_hook_task_table": "hook_task 테이블 정리", "admin.dashboard.cleanup_packages": "만료된 패키지 정리", - "admin.dashboard.cleanup_actions": "만료된 작업 리소스 정리", + "admin.dashboard.cleanup_actions": "만료된 액션 리소스 정리", "admin.dashboard.server_uptime": "서버를 켠 시간", "admin.dashboard.current_goroutine": "현재 Go루틴", "admin.dashboard.current_memory_usage": "현재 메모리 사용율", @@ -2944,10 +2972,10 @@ "admin.dashboard.update_checker": "업데이트 확인", "admin.dashboard.delete_old_system_notices": "데이터베이스에서 모든 오래된 시스템 알림 삭제", "admin.dashboard.gc_lfs": "LFS 메타 객체 가비지 컬렉션", - "admin.dashboard.stop_zombie_tasks": "좀비 작업 동작 중지", + "admin.dashboard.stop_zombie_tasks": "좀비 작업 액션 중지", "admin.dashboard.stop_endless_tasks": "끝나지 않는 작업 중지", - "admin.dashboard.cancel_abandoned_jobs": "포기한 작업 동작 취소", - "admin.dashboard.start_schedule_tasks": "예약된 작업 동작 시작", + "admin.dashboard.cancel_abandoned_jobs": "포기한 작업 액션 취소", + "admin.dashboard.start_schedule_tasks": "예약된 작업 액션 시작", "admin.dashboard.sync_branch.started": "브랜치 동기화 시작됨", "admin.dashboard.sync_tag.started": "태그 동기화 시작됨", "admin.dashboard.rebuild_issue_indexer": "이슈 인덱서 재구축", @@ -3611,7 +3639,7 @@ "packages.owner.settings.chef.keypair": "키 쌍 생성", "packages.owner.settings.chef.keypair.description": "Chef 레지스트리에 인증하려면 키 쌍이 필요합니다. 이전에 키 쌍을 생성한 경우, 새 키 쌍를 생성하면 이전 키 쌍은 폐기됩니다.", "secrets.secrets": "비밀", - "secrets.description": "비밀 키는 특정 동작에 전달되며 다른 방법으로는 읽을 수 없습니다.", + "secrets.description": "비밀 키는 특정 액션에 전달되며 다른 방법으로는 읽을 수 없습니다.", "secrets.none": "아직 비밀이 없습니다.", "secrets.creation.description": "설명", "secrets.creation.name_placeholder": "대소문자를 구분하지 않으며, 영숫자 또는 밑줄 문자만, GITEA_ 또는 GITHUB_로 시작할 수 없음", @@ -3626,8 +3654,8 @@ "secrets.deletion.success": "비밀이 삭제되었습니다.", "secrets.deletion.failed": "비밀 제거에 실패했습니다.", "secrets.management": "비밀 관리", - "actions.actions": "동작", - "actions.unit.desc": "동작 관리", + "actions.actions": "액션", + "actions.unit.desc": "액션 관리", "actions.status.unknown": "알수없음", "actions.status.waiting": "대기 중", "actions.status.running": "실행중", @@ -3703,10 +3731,14 @@ "actions.runs.expire_log_message": "로그가 너무 오래되어 제거되었습니다.", "actions.runs.delete": "워크플로우 실행 삭제", "actions.runs.cancel": "워크플로우 실행 취소", - "actions.runs.delete.description": "이 워크플로우 실행을 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "actions.runs.delete.description": "이 워크플로우 실행을 영구적으로 삭제하시겠습니까? 이 동작은 되돌릴 수 없습니다.", "actions.runs.not_done": "이 워크플로우 실행은 완료되지 않았습니다.", "actions.runs.view_workflow_file": "워크플로우 파일 표시", "actions.runs.workflow_graph": "워크플로우 그래프", + "actions.runs.summary": "요약", + "actions.runs.all_jobs": "모든 작업", + "actions.runs.triggered_via": "%s를 통해 트리거됨", + "actions.runs.total_duration": "총기간:", "actions.workflow.disable": "워크플로 비활성화", "actions.workflow.disable_success": "워크플로 '%s'가 성공적으로 비활성화되었습니다.", "actions.workflow.enable": "워크플로 활성화", @@ -3726,7 +3758,7 @@ "actions.variables.none": "아직 변수가 없습니다.", "actions.variables.deletion": "변수 제거", "actions.variables.deletion.description": "변수 제거는 영구적이며 되돌릴 수 없습니다. 계속하시겠습니까?", - "actions.variables.description": "변수는 특정 동작에 전달되며 다른 방법으로는 읽을 수 없습니다.", + "actions.variables.description": "변수는 특정 액션에 전달되며 다른 방법으로는 읽을 수 없습니다.", "actions.variables.id_not_exist": "ID %d인 변수가 존재하지 않습니다.", "actions.variables.edit": "변수 수정", "actions.variables.deletion.failed": "변수 제거에 실패했습니다.", @@ -3738,7 +3770,7 @@ "actions.logs.always_auto_scroll": "로그 자동 스크롤 항상 켜기", "actions.logs.always_expand_running": "실행 중인 로그 항상 펼침", "actions.general": "일반", - "actions.general.enable_actions": "동작 활성화", + "actions.general.enable_actions": "액션 활성화", "actions.general.collaborative_owners_management": "보조 소유자 관리", "actions.general.collaborative_owners_management_help": "보조 소유자는 이 리포지토리의 액션 및 워크플로에 접근할 수 있는 개인 리포지토리를 가진 사용자 또는 조직입니다.", "actions.general.add_collaborative_owner": "보조 소유자 추가", @@ -3756,5 +3788,24 @@ "git.filemode.normal_file": "일반", "git.filemode.executable_file": "실행파일", "git.filemode.symbolic_link": "Symlink", - "git.filemode.submodule": "서브모듈" + "git.filemode.submodule": "서브모듈", + "org.repos.none": "리포지토리 없음.", + "actions.general.permissions": "액션 토큰 권한", + "actions.general.token_permissions.mode": "기본 토큰 권한", + "actions.general.token_permissions.mode.desc": "워크플로 파일에서 권한을 선언하지 않으면 액션 작업은 기본 권한을 사용합니다.", + "actions.general.token_permissions.mode.permissive": "허용적", + "actions.general.token_permissions.mode.permissive.desc": "작업 리포지토리에 대한 읽기 쓰기 권한.", + "actions.general.token_permissions.mode.restricted": "제한됨", + "actions.general.token_permissions.mode.restricted.desc": "작업 리포지토리의 콘텐츠 단위(코드, 릴리스)에 대한 읽기 전용 권한.", + "actions.general.token_permissions.override_owner": "소유자 수준의 구성 오버라이드", + "actions.general.token_permissions.override_owner_desc": "활성화하면, 이 리포지토리는 다음 소유자-수준(사용자 혹은 조직) 구성 대신에 자신의 액션 구성을 사용합니다.", + "actions.general.token_permissions.maximum": "최대 토큰 권한", + "actions.general.token_permissions.maximum.description": "액션 작업의 효과 권한은 최대 권한으로 제한됩니다.", + "actions.general.token_permissions.fork_pr_note": "포크로부터 생성된 풀 리퀘스트로 작업이 시작된 경우, 해당 작업의 유효 권한은 읽기 전용 권한을 초과하지 않습니다.", + "actions.general.token_permissions.customize_max_permissions": "최대 권한을 커스터마이징", + "actions.general.cross_repo": "크로스 리포지토리 억세스", + "actions.general.cross_repo_desc": "선택된 저장소가 이 소유자의 모든 저장소에서 액션 작업을 실행할 때 GITEA_TOKEN을 사용하여 (읽기 전용으로) 액세스할 수 있도록 허용합니다.", + "actions.general.cross_repo_selected": "선택된 리포지토리", + "actions.general.cross_repo_target_repos": "대상 리포지토리", + "actions.general.cross_repo_add": "대상 리포지토리 추가" } From c453d09c36fad094405314dba2f370434b200711 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 23 Mar 2026 21:08:48 -0700 Subject: [PATCH 110/207] Catch scanner error when possible to avoid bypass (#36963) --- cmd/hook.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/hook.go b/cmd/hook.go index a0280e283f..992e52c279 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -276,6 +276,9 @@ Gitea or set your environment appropriately.`, "") lastline = 0 } } + if err := scanner.Err(); err != nil { + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count > 0 { hookOptions.OldCommitIDs = oldCommitIDs[:count] @@ -415,6 +418,11 @@ Gitea or set your environment appropriately.`, "") count = 0 } } + if err := scanner.Err(); err != nil { + _ = dWriter.Close() + hookPrintResults(results) + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count == 0 { if wasEmpty && masterPushed { From 66b8178e59cf7b44528fecd93637d299582c991e Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 17:49:29 +0100 Subject: [PATCH 111/207] Improve AGENTS.md (#36974) 1. Remove header line, useless context bloat 2. Reword all "before commiting" lines because some people may not be using the agent to commit, only to write changes. --- AGENTS.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 402a9d6945..3db66637b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,8 @@ -# Instructions for agents - - Use `make help` to find available development targets -- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint -- Before committing `.ts` changes, run `make lint-js` to lint -- Before committing `go.mod` changes, run `make tidy` -- Before committing new `.go` files, add the current year into the copyright header -- Before committing any files, remove all trailing whitespace from source code lines +- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them +- Run `make lint-js` to lint `.ts` files +- Run `make tidy` after any `go.mod` changes +- Add the current year into the copyright header of new `.go` files +- Ensure no trailing whitespace in edited files - Never force-push to pull request branches - Always start issue and pull request comments with an authorship attribution From c96cc701445cfe31fce65dc4ff667ab49f218857 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Wed, 25 Mar 2026 01:23:13 +0800 Subject: [PATCH 112/207] Add class "list-header-filters" to the div for projects (#36889) closes #36886 --- templates/projects/view.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index e1b7364f41..3e1afab79f 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -4,7 +4,7 @@

    {{.Project.Title}}

    - @@ -917,21 +904,8 @@
    -
    - -
    -
    - - -
    - -
    - - -
    + {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm"))}}
    @@ -949,25 +923,13 @@
    -
    - -
    -
    - - -
    + {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}}
    -
    - - -
    + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.transfer_perform"))}}
    @@ -986,49 +948,57 @@
    -
    - -
    -
    - - -
    - -
    - - -
    + {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_delete"))}}
    {{if not .Repository.IsFork}} -
    -
    - -
    -
    - - -
    - -
    - - -
    + {{template "repo/settings/repo_name_confirm_fields" (dict "RepoName" .Repository.Name)}} + {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete"))}}
    diff --git a/templates/repo/settings/repo_name_confirm_fields.tmpl b/templates/repo/settings/repo_name_confirm_fields.tmpl new file mode 100644 index 0000000000..a8f9800e71 --- /dev/null +++ b/templates/repo/settings/repo_name_confirm_fields.tmpl @@ -0,0 +1,10 @@ +
    + +
    +
    + + +
    diff --git a/tests/integration/repo_visibility_test.go b/tests/integration/repo_visibility_test.go new file mode 100644 index 0000000000..e7ad8ddd87 --- /dev/null +++ b/tests/integration/repo_visibility_test.go @@ -0,0 +1,59 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestRepositoryVisibilityChange(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + + t.Run("MakePrivateRequiresCorrectName", func(t *testing.T) { + // Wrong name should be rejected with a JSON error + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{ + "action": "visibility", + "private": "true", + "confirm_repo_name": "wrong-name", + }) + resp := session.MakeRequest(t, req, http.StatusBadRequest) + assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.False(t, repo1.IsPrivate) + + // Correct full name (owner/repo) should succeed with a JSON redirect + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{ + "action": "visibility", + "private": "true", + "confirm_repo_name": "user2/repo1", + }) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) + + repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.True(t, repo1.IsPrivate) + }) + + t.Run("MakePublicDoesNotRequireName", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user2/repo2/settings", map[string]string{ + "action": "visibility", + "private": "false", + }) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + assert.False(t, repo2.IsPrivate) + }) +} From e24c3f7a40dade43ae7d9e3354491184ad64c141 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 25 Mar 2026 08:23:11 +0100 Subject: [PATCH 115/207] Fix org contact email not clearable once set (#36975) When the email field was submitted as empty in org settings (web and API), the previous guard `if form.Email != ""` silently skipped the update, making it impossible to remove a contact email after it was set. --------- Co-authored-by: wxiaoguang --- modules/structs/org.go | 16 +++--- routers/api/v1/org/org.go | 20 ++++--- routers/web/org/setting.go | 20 +++---- services/forms/org.go | 14 ++--- services/org/org.go | 17 ++++++ services/org/org_test.go | 54 ++++++++++++++----- services/user/email.go | 62 +++++++++++----------- services/user/email_test.go | 75 +++++++++++---------------- templates/swagger/v1_json.tmpl | 2 +- tests/integration/api_org_test.go | 55 ++++++++++++-------- tests/integration/integration_test.go | 3 +- tests/integration/org_test.go | 58 ++++++++++++++------- 12 files changed, 232 insertions(+), 164 deletions(-) diff --git a/modules/structs/org.go b/modules/structs/org.go index c3d70ebf00..d79b1d1d1c 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -66,23 +66,21 @@ type CreateOrgOption struct { RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` } -// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged - // EditOrgOption options for editing an organization type EditOrgOption struct { // The full display name of the organization - FullName string `json:"full_name" binding:"MaxSize(100)"` - // The email address of the organization - Email string `json:"email" binding:"MaxSize(255)"` + FullName *string `json:"full_name" binding:"MaxSize(100)"` + // The email address of the organization; use empty string to clear + Email *string `json:"email" binding:"MaxSize(255)"` // The description of the organization - Description string `json:"description" binding:"MaxSize(255)"` + Description *string `json:"description" binding:"MaxSize(255)"` // The website URL of the organization - Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` + Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"` // The location of the organization - Location string `json:"location" binding:"MaxSize(50)"` + Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` // enum: public,limited,private - Visibility string `json:"visibility" binding:"In(,public,limited,private)"` + Visibility *string `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index d42241f054..ce2a2e5580 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -5,6 +5,7 @@ package org import ( + "errors" "net/http" activities_model "code.gitea.io/gitea/models/activities" @@ -14,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/utils" @@ -379,19 +381,21 @@ func Edit(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditOrgOption) - if form.Email != "" { - if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil { - ctx.APIErrorInternal(err) + if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusUnprocessableEntity, err) return } + ctx.APIErrorInternal(err) + return } opts := &user_service.UpdateOptions{ - FullName: optional.Some(form.FullName), - Description: optional.Some(form.Description), - Website: optional.Some(form.Website), - Location: optional.Some(form.Location), - Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), + FullName: optional.FromPtr(form.FullName), + Description: optional.FromPtr(form.Description), + Website: optional.FromPtr(form.Website), + Location: optional.FromPtr(form.Location), + Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 04baa58b73..ca1da6617e 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -5,6 +5,7 @@ package org import ( + "errors" "net/http" "net/url" @@ -69,24 +70,25 @@ func SettingsPost(ctx *context.Context) { } org := ctx.Org.Organization - - if form.Email != "" { - if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { + if err := org_service.UpdateOrgEmailAddress(ctx, org, form.Email); err != nil { + if errors.Is(err, util.ErrInvalidArgument) { ctx.Data["Err_Email"] = true ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) return } + ctx.ServerError("UpdateOrgEmailAddress", err) + return } opts := &user_service.UpdateOptions{ - FullName: optional.Some(form.FullName), - Description: optional.Some(form.Description), - Website: optional.Some(form.Website), - Location: optional.Some(form.Location), - RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess), + FullName: optional.FromPtr(form.FullName), + Description: optional.FromPtr(form.Description), + Website: optional.FromPtr(form.Website), + Location: optional.FromPtr(form.Location), + RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if ctx.Doer.IsAdmin { - opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation) + opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation) } if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil { diff --git a/services/forms/org.go b/services/forms/org.go index 3997e1da84..8a8106e8cf 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -36,13 +36,13 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding // UpdateOrgSettingForm form for updating organization settings type UpdateOrgSettingForm struct { - FullName string `binding:"MaxSize(100)"` - Email string `binding:"MaxSize(255)"` - Description string `binding:"MaxSize(255)"` - Website string `binding:"ValidUrl;MaxSize(255)"` - Location string `binding:"MaxSize(50)"` - MaxRepoCreation int - RepoAdminChangeTeamAccess bool + FullName *string `binding:"MaxSize(100)"` + Email *string `binding:"MaxSize(255)"` + Description *string `binding:"MaxSize(255)"` + Website *string `binding:"ValidUrl;MaxSize(255)"` + Location *string `binding:"MaxSize(50)"` + MaxRepoCreation *int + RepoAdminChangeTeamAccess *bool } // Validate validates the fields diff --git a/services/org/org.go b/services/org/org.go index 8da77c691c..32c46d7cb9 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -168,3 +168,20 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati return nil }) } + +// UpdateOrgEmailAddress validates and updates the organization's contact email. +// A nil email means no change. +func UpdateOrgEmailAddress(ctx context.Context, org *org_model.Organization, email *string) error { + if email == nil { + return nil + } + + if *email != "" { + if err := user_model.ValidateEmail(*email); err != nil { + return err + } + } + + org.Email = *email + return user_model.UpdateUserCols(ctx, org.AsUser(), "email") +} diff --git a/services/org/org_test.go b/services/org/org_test.go index 5fdc1a6fd5..5253c73902 100644 --- a/services/org/org_test.go +++ b/services/org/org_test.go @@ -10,28 +10,54 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { unittest.MainTest(m) } -func TestDeleteOrganization(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}) - assert.NoError(t, DeleteOrganization(t.Context(), org, false)) - unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6}) - unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6}) - unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6}) +func TestOrg(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) - org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - err := DeleteOrganization(t.Context(), org, false) - assert.Error(t, err) - assert.True(t, repo_model.IsErrUserOwnRepos(err)) + t.Run("UpdateOrgEmailAddress", func(t *testing.T) { + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + originalEmail := org.Email - user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}) - assert.Error(t, DeleteOrganization(t.Context(), user, false)) - unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, nil)) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: originalEmail}) + + newEmail := "contact@org3.example.com" + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, &newEmail)) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail}) + + invalidEmail := "invalid email" + err := UpdateOrgEmailAddress(t.Context(), org, &invalidEmail) + require.ErrorIs(t, err, util.ErrInvalidArgument) + unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail}) + + require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, new(""))) + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: ""}) + assert.Empty(t, org.Email) + }) + + t.Run("DeleteOrganization", func(t *testing.T) { + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}) + assert.NoError(t, DeleteOrganization(t.Context(), org, false)) + unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6}) + unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6}) + unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6}) + + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + err := DeleteOrganization(t.Context(), org, false) + assert.Error(t, err) + assert.True(t, repo_model.IsErrUserOwnRepos(err)) + + user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}) + assert.Error(t, DeleteOrganization(t.Context(), user, false)) + unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) + }) } diff --git a/services/user/email.go b/services/user/email.go index c45b3b3ec9..927e4b7234 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -14,7 +14,14 @@ import ( "code.gitea.io/gitea/modules/util" ) +// ReplacePrimaryEmailAddress replaces the user's primary email address with the given email address. +// It also updates the user's email field to match the new primary email address. func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error { + // FIXME: this check is from old logic, but it is not right, there are far more user types, not only "organization" + if u.IsOrganization() { + return util.NewInvalidArgumentErrorf("user %s is an organization", u.Name) + } + if strings.EqualFold(u.Email, emailStr) { return nil } @@ -24,41 +31,38 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt } return db.WithTx(ctx, func(ctx context.Context) error { - if !u.IsOrganization() { - // Check if address exists already - email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) - if err != nil && !errors.Is(err, util.ErrNotExist) { - return err - } - if email != nil { - if email.IsPrimary && email.UID == u.ID { - return nil - } - return user_model.ErrEmailAlreadyUsed{Email: emailStr} + // Check if address exists already + email, err := user_model.GetEmailAddressByEmail(ctx, emailStr) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } + if email != nil { + if email.IsPrimary && email.UID == u.ID { + return nil } + return user_model.ErrEmailAlreadyUsed{Email: emailStr} + } - // Remove old primary address - primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) - if err != nil { - return err - } - if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { - return err - } + // Remove old primary address + primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID) + if err != nil { + return err + } + if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil { + return err + } - // Insert new primary address - if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{ - UID: u.ID, - Email: emailStr, - IsActivated: true, - IsPrimary: true, - }); err != nil { - return err - } + // Insert new primary address + if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{ + UID: u.ID, + Email: emailStr, + IsActivated: true, + IsPrimary: true, + }); err != nil { + return err } u.Email = emailStr - return user_model.UpdateUserCols(ctx, u, "email") }) } diff --git a/services/user/email_test.go b/services/user/email_test.go index a031b12cad..9fb0560b05 100644 --- a/services/user/email_test.go +++ b/services/user/email_test.go @@ -6,17 +6,16 @@ package user import ( "testing" - organization_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) -func TestReplacePrimaryEmailAddress(t *testing.T) { +func TestUserEmail(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - t.Run("User", func(t *testing.T) { + t.Run("PrimaryEmailAddress", func(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) emails, err := user_model.GetEmailAddresses(t.Context(), user.ID) @@ -42,50 +41,36 @@ func TestReplacePrimaryEmailAddress(t *testing.T) { assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), user, "primary-13@example.com")) }) - t.Run("Organization", func(t *testing.T) { - org := unittest.AssertExistsAndLoadBean(t, &organization_model.Organization{ID: 3}) + t.Run("AddEmailAddresses", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.Equal(t, "org3@example.com", org.Email) + assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "})) - assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), org.AsUser(), "primary-org@example.com")) + emails := []string{"user1234@example.com", "user5678@example.com"} - assert.Equal(t, "primary-org@example.com", org.Email) + assert.NoError(t, AddEmailAddresses(t.Context(), user, emails)) + + err := AddEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) + }) + + t.Run("DeleteEmailAddresses", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + emails := []string{"user2-2@example.com"} + + err := DeleteEmailAddresses(t.Context(), user, emails) + assert.NoError(t, err) + + err = DeleteEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrEmailAddressNotExist(err)) + + emails = []string{"user2@example.com"} + + err = DeleteEmailAddresses(t.Context(), user, emails) + assert.Error(t, err) + assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err)) }) } - -func TestAddEmailAddresses(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "})) - - emails := []string{"user1234@example.com", "user5678@example.com"} - - assert.NoError(t, AddEmailAddresses(t.Context(), user, emails)) - - err := AddEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrEmailAlreadyUsed(err)) -} - -func TestDeleteEmailAddresses(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - emails := []string{"user2-2@example.com"} - - err := DeleteEmailAddresses(t.Context(), user, emails) - assert.NoError(t, err) - - err = DeleteEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrEmailAddressNotExist(err)) - - emails = []string{"user2@example.com"} - - err = DeleteEmailAddresses(t.Context(), user, emails) - assert.Error(t, err) - assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err)) -} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 20db48b91a..adc6c18175 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -24855,7 +24855,7 @@ "x-go-name": "Description" }, "email": { - "description": "The email address of the organization", + "description": "The email address of the organization; use empty string to clear", "type": "string", "x-go-name": "Email" }, diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index 6b7826fbb8..42f9e4cbf6 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) { }) t.Run("OrgEdit", func(t *testing.T) { - org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "private", - } - req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token) - resp := MakeRequest(t, req, http.StatusOK) + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) + assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility) - var apiOrg api.Organization - DecodeJSON(t, resp, &apiOrg) + org3Edit := api.EditOrgOption{ + FullName: new("new full name"), + Description: new("new description"), + Website: new("https://org3-new-website.example.com"), + Location: new("new location"), + Visibility: new("limited"), + Email: new("org3-new-email@example.com"), + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token) + resp := MakeRequest(t, req, http.StatusOK) + apiOrg := DecodeJSON(t, resp, &api.Organization{}) assert.Equal(t, "org3", apiOrg.Name) - assert.Equal(t, org.FullName, apiOrg.FullName) - assert.Equal(t, org.Description, apiOrg.Description) - assert.Equal(t, org.Website, apiOrg.Website) - assert.Equal(t, org.Location, apiOrg.Location) - assert.Equal(t, org.Visibility, apiOrg.Visibility) + assert.Equal(t, *org3Edit.FullName, apiOrg.FullName) + assert.Equal(t, *org3Edit.Description, apiOrg.Description) + assert.Equal(t, *org3Edit.Website, apiOrg.Website) + assert.Equal(t, *org3Edit.Location, apiOrg.Location) + assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility) + assert.Equal(t, *org3Edit.Email, apiOrg.Email) + org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) + assert.Equal(t, api.VisibleTypeLimited, org3.Visibility) + + // empty email can clear the email, nil fields won't change the settings + req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{ + Email: new(""), + }).AddTokenAuth(user1Token) + resp = MakeRequest(t, req, http.StatusOK) + apiOrg = DecodeJSON(t, resp, &api.Organization{}) + assert.Equal(t, *org3Edit.FullName, apiOrg.FullName) + assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility) + assert.Empty(t, apiOrg.Email) }) - t.Run("OrgEditBadVisibility", func(t *testing.T) { + t.Run("OrgEditInvalidVisibility", func(t *testing.T) { org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "badvisibility", + Visibility: new("invalid-visibility"), } req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token) MakeRequest(t, req, http.StatusUnprocessableEntity) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index a4807814df..e8b0cbd641 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -410,12 +410,13 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { } } -func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) { +func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) { t.Helper() // FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names decoder := json.NewDecoderCaseInsensitive(resp.Body) require.NoError(t, decoder.Decode(v)) + return v } func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) { diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go index 3ed7baa5ba..cedc0406ca 100644 --- a/tests/integration/org_test.go +++ b/tests/integration/org_test.go @@ -23,9 +23,18 @@ import ( "github.com/stretchr/testify/require" ) -func TestOrgRepos(t *testing.T) { +func TestOrg(t *testing.T) { defer tests.PrepareTestEnv(t)() + t.Run("OrgRepos", testOrgRepos) + t.Run("PrivateOrg", testPrivateOrg) + t.Run("LimitedOrg", testLimitedOrg) + t.Run("OrgMembers", testOrgMembers) + t.Run("OrgRestrictedUser", testOrgRestrictedUser) + t.Run("TeamSearch", testTeamSearch) + t.Run("OrgSettings", testOrgSettings) +} +func testOrgRepos(t *testing.T) { var ( users = []string{"user1", "user2"} cases = map[string][]string{ @@ -53,10 +62,8 @@ func TestOrgRepos(t *testing.T) { } } -func TestLimitedOrg(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testLimitedOrg(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/limited_org") MakeRequest(t, req, http.StatusNotFound) req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org") @@ -83,10 +90,8 @@ func TestLimitedOrg(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestPrivateOrg(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testPrivateOrg(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/privated_org") MakeRequest(t, req, http.StatusNotFound) req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org") @@ -122,10 +127,8 @@ func TestPrivateOrg(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestOrgMembers(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // not logged in user +func testOrgMembers(t *testing.T) { + // not logged-in user req := NewRequest(t, "GET", "/org/org25/members") MakeRequest(t, req, http.StatusOK) @@ -140,9 +143,7 @@ func TestOrgMembers(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestOrgRestrictedUser(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testOrgRestrictedUser(t *testing.T) { // privated_org is a private org who has id 23 orgName := "privated_org" @@ -200,9 +201,7 @@ func TestOrgRestrictedUser(t *testing.T) { restrictedSession.MakeRequest(t, req, http.StatusOK) } -func TestTeamSearch(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testTeamSearch(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17}) @@ -251,3 +250,24 @@ func TestTeamSearch(t *testing.T) { assert.Len(t, teams, 1) // team permission is "write", so can write "code" }) } + +func testOrgSettings(t *testing.T) { + session := loginUser(t, "user2") + + req := NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{ + "full_name": "org3 new full name", + "email": "org3-new-email@example.com", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + assert.Equal(t, "org3 new full name", org.FullName) + assert.Equal(t, "org3-new-email@example.com", org.Email) + + req = NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{ + "email": "", // empty email means "clear email" + }) + session.MakeRequest(t, req, http.StatusSeeOther) + org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + assert.Equal(t, "org3 new full name", org.FullName) + assert.Empty(t, org.Email) +} From bb1e22bba4cbdf85e86b96cd39a82f2a1a4a8b8d Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 25 Mar 2026 08:40:46 +0100 Subject: [PATCH 116/207] Allow text selection on checkbox labels (#36970) Remove `user-select: none` from checkbox labels to allow text selection which is sometimes useful. --------- Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.6) --- web_src/css/modules/checkbox.css | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css index 558486e63a..220abfc17d 100644 --- a/web_src/css/modules/checkbox.css +++ b/web_src/css/modules/checkbox.css @@ -101,7 +101,6 @@ input[type="checkbox"]:indeterminate::before { cursor: auto; position: relative; display: block; - user-select: none; } .ui.checkbox label, From 435123fe65f7f0c94d5ee39a4b5d638803d4bd95 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Wed, 25 Mar 2026 10:53:13 -0400 Subject: [PATCH 117/207] Switch `cmd/` to use constructor functions. (#36962) This is a step towards potentially splitting command groups into their own folders to clean up `cmd/` as one folder for all cli commands. Returning fresh command instances will also aid in adding tests as you don't need to concern yourself with the whole command tree being one mutable variable. --------- Co-authored-by: wxiaoguang --- cmd/actions.go | 13 +- cmd/admin.go | 41 +++-- cmd/admin_auth.go | 11 +- cmd/admin_regenerate.go | 10 +- cmd/admin_user.go | 24 +-- cmd/admin_user_generate_access_token.go | 52 +++--- cmd/admin_user_list.go | 20 +-- cmd/docs.go | 33 ++-- cmd/doctor.go | 118 +++++++------- cmd/doctor_convert.go | 13 +- cmd/doctor_test.go | 2 +- cmd/dump.go | 143 ++++++++--------- cmd/dump_repo.go | 107 ++++++------- cmd/embedded.go | 29 ++-- cmd/generate.go | 31 ++-- cmd/hook.go | 36 +++-- cmd/keys.go | 68 ++++---- cmd/main.go | 35 +++-- cmd/manager.go | 44 ++++-- cmd/manager_logging.go | 14 +- cmd/migrate.go | 13 +- cmd/migrate_storage.go | 201 ++++++++++++------------ cmd/restore_repo.go | 65 ++++---- cmd/serv.go | 31 ++-- cmd/web.go | 69 ++++---- tests/integration/cmd_keys_test.go | 6 +- 26 files changed, 651 insertions(+), 578 deletions(-) diff --git a/cmd/actions.go b/cmd/actions.go index 2c51c6a1bc..44b6b7b54c 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -13,17 +13,18 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdActions represents the available actions sub-commands. - CmdActions = &cli.Command{ +func newActionsCommand() *cli.Command { + return &cli.Command{ Name: "actions", Usage: "Manage Gitea Actions", Commands: []*cli.Command{ - subcmdActionsGenRunnerToken, + newActionsGenerateRunnerTokenCommand(), }, } +} - subcmdActionsGenRunnerToken = &cli.Command{ +func newActionsGenerateRunnerTokenCommand() *cli.Command { + return &cli.Command{ Name: "generate-runner-token", Usage: "Generate a new token for a runner to use to register with the server", Action: runGenerateActionsRunnerToken, @@ -37,7 +38,7 @@ var ( }, }, } -) +} func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { setting.MustInstalled() diff --git a/cmd/admin.go b/cmd/admin.go index dbd48e5727..c0e21731cb 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -18,36 +18,41 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdAdmin represents the available admin sub-command. - CmdAdmin = &cli.Command{ +func newAdminCommand() *cli.Command { + return &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", Commands: []*cli.Command{ - subcmdUser, - subcmdRepoSyncReleases, - subcmdRegenerate, - subcmdAuth, - subcmdSendMail, + newUserCommand(), + newRepoSyncReleasesCommand(), + newRegenerateCommand(), + newAuthCommand(), + newSendMailCommand(), }, } +} - subcmdRepoSyncReleases = &cli.Command{ +func newRepoSyncReleasesCommand() *cli.Command { + return &cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", Action: runRepoSyncReleases, } +} - subcmdRegenerate = &cli.Command{ +func newRegenerateCommand() *cli.Command { + return &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", Commands: []*cli.Command{ - microcmdRegenHooks, - microcmdRegenKeys, + newRegenerateHooksCommand(), + newRegenerateKeysCommand(), }, } +} - subcmdAuth = &cli.Command{ +func newAuthCommand() *cli.Command { + return &cli.Command{ Name: "auth", Usage: "Modify external auth providers", Commands: []*cli.Command{ @@ -59,12 +64,14 @@ var ( microcmdAuthUpdateLdapSimpleAuth(), microcmdAuthAddSMTP(), microcmdAuthUpdateSMTP(), - microcmdAuthList, - microcmdAuthDelete, + newAuthListCommand(), + newAuthDeleteCommand(), }, } +} - subcmdSendMail = &cli.Command{ +func newSendMailCommand() *cli.Command { + return &cli.Command{ Name: "sendmail", Usage: "Send a message to all users", Action: runSendMail, @@ -86,7 +93,7 @@ var ( }, }, } -) +} func idFlag() *cli.Int64Flag { return &cli.Int64Flag{ diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index 1a09366722..b55bb1481c 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -17,14 +17,17 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdAuthDelete = &cli.Command{ +func newAuthDeleteCommand() *cli.Command { + return &cli.Command{ Name: "delete", Usage: "Delete specific auth source", Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } - microcmdAuthList = &cli.Command{ +} + +func newAuthListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List auth sources", Action: runListAuth, @@ -55,7 +58,7 @@ var ( }, }, } -) +} func runListAuth(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index a5f1bd5105..aa235441ba 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -13,19 +13,21 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdRegenHooks = &cli.Command{ +func newRegenerateHooksCommand() *cli.Command { + return &cli.Command{ Name: "hooks", Usage: "Regenerate git-hooks", Action: runRegenerateHooks, } +} - microcmdRegenKeys = &cli.Command{ +func newRegenerateKeysCommand() *cli.Command { + return &cli.Command{ Name: "keys", Usage: "Regenerate authorized_keys file", Action: runRegenerateKeys, } -) +} func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 3a24c3e56f..8dd8bb4eca 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -7,15 +7,17 @@ import ( "github.com/urfave/cli/v3" ) -var subcmdUser = &cli.Command{ - Name: "user", - Usage: "Modify users", - Commands: []*cli.Command{ - microcmdUserCreate(), - microcmdUserList, - microcmdUserChangePassword(), - microcmdUserDelete(), - microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword(), - }, +func newUserCommand() *cli.Command { + return &cli.Command{ + Name: "user", + Usage: "Modify users", + Commands: []*cli.Command{ + microcmdUserCreate(), + newUserListCommand(), + microcmdUserChangePassword(), + microcmdUserDelete(), + newUserGenerateAccessTokenCommand(), + microcmdUserMustChangePassword(), + }, + } } diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 61064fdef4..7f8330ff7e 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -14,32 +14,34 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserGenerateAccessToken = &cli.Command{ - Name: "generate-access-token", - Usage: "Generate an access token for a specific user", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username", +func newUserGenerateAccessTokenCommand() *cli.Command { + return &cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username", + }, + &cli.StringFlag{ + Name: "token-name", + Aliases: []string{"t"}, + Usage: "Token name", + Value: "gitea-admin", + }, + &cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + &cli.StringFlag{ + Name: "scopes", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, + }, }, - &cli.StringFlag{ - Name: "token-name", - Aliases: []string{"t"}, - Usage: "Token name", - Value: "gitea-admin", - }, - &cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - &cli.StringFlag{ - Name: "scopes", - Value: "all", - Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, - }, - }, - Action: runGenerateAccessToken, + Action: runGenerateAccessToken, + } } func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go index e3d345e2f2..2958fe0cc8 100644 --- a/cmd/admin_user_list.go +++ b/cmd/admin_user_list.go @@ -14,16 +14,18 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserList = &cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", +func newUserListCommand() *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, }, - }, + } } func runListUsers(ctx context.Context, c *cli.Command) error { diff --git a/cmd/docs.go b/cmd/docs.go index 098c0e9a8a..8e0f9428df 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -13,23 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDocs represents the available docs sub-command. -var CmdDocs = &cli.Command{ - Name: "docs", - Usage: "Output CLI documentation", - Description: "A command to output Gitea's CLI documentation, optionally to a file.", - Action: runDocs, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "man", - Usage: "Output man pages instead", +func newDocsCommand() *cli.Command { + return &cli.Command{ + Name: "docs", + Usage: "Output CLI documentation", + Description: "A command to output Gitea's CLI documentation, optionally to a file.", + Action: runDocs, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "man", + Usage: "Output man pages instead", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Path to output to instead of stdout (will overwrite if exists)", + }, }, - &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "Path to output to instead of stdout (will overwrite if exists)", - }, - }, + } } func runDocs(_ context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor.go b/cmd/doctor.go index 596dd61178..188740dbce 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -24,73 +24,77 @@ import ( "xorm.io/xorm" ) -// CmdDoctor represents the available doctor sub-command. -var CmdDoctor = &cli.Command{ - Name: "doctor", - 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.", - - Commands: []*cli.Command{ - cmdDoctorCheck, - cmdRecreateTable, - cmdDoctorConvert, - }, +func newDoctorCommand() *cli.Command { + return &cli.Command{ + Name: "doctor", + 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.", + Commands: []*cli.Command{ + newDoctorCheckCommand(), + newRecreateTableCommand(), + newDoctorConvertCommand(), + }, + } } -var cmdDoctorCheck = &cli.Command{ - Name: "check", - Usage: "Diagnose and optionally fix problems", - 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.", - Action: runDoctorCheck, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "list", - Usage: "List the available checks", +func newDoctorCheckCommand() *cli.Command { + return &cli.Command{ + Name: "check", + Usage: "Diagnose and optionally fix problems", + 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.", + Action: runDoctorCheck, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "list", + Usage: "List the available checks", + }, + &cli.BoolFlag{ + Name: "default", + Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", + }, + &cli.StringSliceFlag{ + Name: "run", + Usage: "Run the provided checks - (if --default is set, the default checks will also run)", + }, + &cli.BoolFlag{ + Name: "all", + Usage: "Run all the available checks", + }, + &cli.BoolFlag{ + Name: "fix", + Usage: "Automatically fix what we can", + }, + &cli.StringFlag{ + Name: "log-file", + Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, + }, + &cli.BoolFlag{ + Name: "color", + Aliases: []string{"H"}, + Usage: "Use color for outputted information", + }, }, - &cli.BoolFlag{ - Name: "default", - Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", - }, - &cli.StringSliceFlag{ - Name: "run", - Usage: "Run the provided checks - (if --default is set, the default checks will also run)", - }, - &cli.BoolFlag{ - Name: "all", - Usage: "Run all the available checks", - }, - &cli.BoolFlag{ - Name: "fix", - Usage: "Automatically fix what we can", - }, - &cli.StringFlag{ - Name: "log-file", - Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, - }, - &cli.BoolFlag{ - Name: "color", - Aliases: []string{"H"}, - Usage: "Use color for outputted information", - }, - }, + } } -var cmdRecreateTable = &cli.Command{ - Name: "recreate-table", - Usage: "Recreate tables from XORM definitions and copy the data.", - ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Usage: "Print SQL commands sent", +func newRecreateTableCommand() *cli.Command { + return &cli.Command{ + Name: "recreate-table", + Usage: "Recreate tables from XORM definitions and copy the data.", + ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Print SQL commands sent", + }, }, - }, - Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. + Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. This command will cause Xorm to recreate tables, copying over the data and deleting the old table. You should back-up your database before doing this and ensure that your database is up-to-date first.`, - Action: runRecreateTable, + Action: runRecreateTable, + } } func runRecreateTable(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go index 8cb718d383..f4867912ab 100644 --- a/cmd/doctor_convert.go +++ b/cmd/doctor_convert.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// cmdDoctorConvert represents the available convert sub-command. -var cmdDoctorConvert = &cli.Command{ - Name: "convert", - Usage: "Convert the database", - Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", - Action: runDoctorConvert, +func newDoctorConvertCommand() *cli.Command { + return &cli.Command{ + Name: "convert", + Usage: "Convert the database", + Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", + Action: runDoctorConvert, + } } func runDoctorConvert(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index da942b38b6..7d2f358947 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -23,7 +23,7 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) app := &cli.Command{ - Commands: []*cli.Command{cmdDoctorCheck}, + Commands: []*cli.Command{newDoctorCheckCommand()}, } err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) assert.NoError(t, err) diff --git a/cmd/dump.go b/cmd/dump.go index 7f0b23ed98..49f4d9e894 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -23,78 +23,79 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDump represents the available dump sub-command. -var CmdDump = &cli.Command{ - Name: "dump", - Usage: "Dump Gitea files and database", - Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, - Action: runDump, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, +func newDumpCommand() *cli.Command { + return &cli.Command{ + Name: "dump", + Usage: "Dump Gitea files and database", + Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, + Action: runDump, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, + }, + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"V"}, + Usage: "Show process details", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display warnings and errors", + }, + &cli.StringFlag{ + Name: "tempdir", + Aliases: []string{"t"}, + Value: os.TempDir(), + Usage: "Temporary dir path", + }, + &cli.StringFlag{ + Name: "database", + Aliases: []string{"d"}, + Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", + }, + &cli.BoolFlag{ + Name: "skip-repository", + Aliases: []string{"R"}, + Usage: "Skip the repository dumping", + }, + &cli.BoolFlag{ + Name: "skip-log", + Aliases: []string{"L"}, + Usage: "Skip the log dumping", + }, + &cli.BoolFlag{ + Name: "skip-custom-dir", + Usage: "Skip custom directory", + }, + &cli.BoolFlag{ + Name: "skip-lfs-data", + Usage: "Skip LFS data", + }, + &cli.BoolFlag{ + Name: "skip-attachment-data", + Usage: "Skip attachment data", + }, + &cli.BoolFlag{ + Name: "skip-package-data", + Usage: "Skip package data", + }, + &cli.BoolFlag{ + Name: "skip-index", + Usage: "Skip bleve index data", + }, + &cli.BoolFlag{ + Name: "skip-db", + Usage: "Skip database", + }, + &cli.StringFlag{ + Name: "type", + Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), + }, }, - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"V"}, - Usage: "Show process details", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display warnings and errors", - }, - &cli.StringFlag{ - Name: "tempdir", - Aliases: []string{"t"}, - Value: os.TempDir(), - Usage: "Temporary dir path", - }, - &cli.StringFlag{ - Name: "database", - Aliases: []string{"d"}, - Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", - }, - &cli.BoolFlag{ - Name: "skip-repository", - Aliases: []string{"R"}, - Usage: "Skip the repository dumping", - }, - &cli.BoolFlag{ - Name: "skip-log", - Aliases: []string{"L"}, - Usage: "Skip the log dumping", - }, - &cli.BoolFlag{ - Name: "skip-custom-dir", - Usage: "Skip custom directory", - }, - &cli.BoolFlag{ - Name: "skip-lfs-data", - Usage: "Skip LFS data", - }, - &cli.BoolFlag{ - Name: "skip-attachment-data", - Usage: "Skip attachment data", - }, - &cli.BoolFlag{ - Name: "skip-package-data", - Usage: "Skip package data", - }, - &cli.BoolFlag{ - Name: "skip-index", - Usage: "Skip bleve index data", - }, - &cli.BoolFlag{ - Name: "skip-db", - Usage: "Skip database", - }, - &cli.StringFlag{ - Name: "type", - Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), - }, - }, + } } func fatal(format string, args ...any) { diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index beda305c85..367454366e 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -22,61 +22,62 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDumpRepository represents the available dump repository sub-command. -var CmdDumpRepository = &cli.Command{ - Name: "dump-repo", - Usage: "Dump the repository from git/github/gitea/gitlab", - Description: "This is a command for dumping the repository data.", - Action: runDumpRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "git_service", - Value: "", - Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", - }, - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to store the data", - }, - &cli.StringFlag{ - Name: "clone_addr", - Value: "", - Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", - }, - &cli.StringFlag{ - Name: "auth_username", - Value: "", - Usage: "The username to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_password", - Value: "", - Usage: "The password to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_token", - Value: "", - Usage: "The personal token to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "The data will be stored on a directory with owner name if not empty", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "The data will be stored on a directory with repository name if not empty", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be migrated, one or more units should be separated as comma. +func newDumpRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "dump-repo", + Usage: "Dump the repository from git/github/gitea/gitlab", + Description: "This is a command for dumping the repository data.", + Action: runDumpRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "git_service", + Value: "", + Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", + }, + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to store the data", + }, + &cli.StringFlag{ + Name: "clone_addr", + Value: "", + Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", + }, + &cli.StringFlag{ + Name: "auth_username", + Value: "", + Usage: "The username to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_password", + Value: "", + Usage: "The password to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_token", + Value: "", + Usage: "The personal token to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "The data will be stored on a directory with owner name if not empty", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "The data will be stored on a directory with repository name if not empty", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, }, - }, + } } func runDumpRepository(ctx context.Context, cmd *cli.Command) error { diff --git a/cmd/embedded.go b/cmd/embedded.go index 9180407fd1..e2110756b8 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -23,20 +23,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdEmbedded represents the available extract sub-command. -var ( - CmdEmbedded = &cli.Command{ +var matchedAssetFiles []assetFile + +func newEmbeddedCommand() *cli.Command { + return &cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", Commands: []*cli.Command{ - subcmdList, - subcmdView, - subcmdExtract, + newEmbeddedListCommand(), + newEmbeddedViewCommand(), + newEmbeddedExtractCommand(), }, } +} - subcmdList = &cli.Command{ +func newEmbeddedListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List files matching the given pattern", Action: runList, @@ -48,8 +51,10 @@ var ( }, }, } +} - subcmdView = &cli.Command{ +func newEmbeddedViewCommand() *cli.Command { + return &cli.Command{ Name: "view", Usage: "View a file matching the given pattern", Action: runView, @@ -61,8 +66,10 @@ var ( }, }, } +} - subcmdExtract = &cli.Command{ +func newEmbeddedExtractCommand() *cli.Command { + return &cli.Command{ Name: "extract", Usage: "Extract resources", Action: runExtract, @@ -91,9 +98,7 @@ var ( }, }, } - - matchedAssetFiles []assetFile -) +} type assetFile struct { fs *assetfs.LayeredFS diff --git a/cmd/generate.go b/cmd/generate.go index 9cb4cf3917..b94ff79aae 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -15,45 +15,52 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdGenerate represents the available generate sub-command. - CmdGenerate = &cli.Command{ +func newGenerateCommand() *cli.Command { + return &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", Commands: []*cli.Command{ - subcmdSecret, + newGenerateSecretCommand(), }, } +} - subcmdSecret = &cli.Command{ +func newGenerateSecretCommand() *cli.Command { + return &cli.Command{ Name: "secret", Usage: "Generate a secret token", Commands: []*cli.Command{ - microcmdGenerateInternalToken, - microcmdGenerateLfsJwtSecret, - microcmdGenerateSecretKey, + newGenerateInternalTokenCommand(), + newGenerateLfsJWTSecretCommand(), + newGenerateSecretKeyCommand(), }, } +} - microcmdGenerateInternalToken = &cli.Command{ +func newGenerateInternalTokenCommand() *cli.Command { + return &cli.Command{ Name: "INTERNAL_TOKEN", Usage: "Generate a new INTERNAL_TOKEN", Action: runGenerateInternalToken, } +} - microcmdGenerateLfsJwtSecret = &cli.Command{ +func newGenerateLfsJWTSecretCommand() *cli.Command { + return &cli.Command{ Name: "JWT_SECRET", Aliases: []string{"LFS_JWT_SECRET"}, Usage: "Generate a new JWT_SECRET", Action: runGenerateLfsJwtSecret, } +} - microcmdGenerateSecretKey = &cli.Command{ +func newGenerateSecretKeyCommand() *cli.Command { + return &cli.Command{ Name: "SECRET_KEY", Usage: "Generate a new SECRET_KEY", Action: runGenerateSecretKey, } -) +} func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() diff --git a/cmd/hook.go b/cmd/hook.go index 992e52c279..4a6c7c2905 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -28,23 +28,24 @@ const ( hookBatchSize = 500 ) -var ( - // CmdHook represents the available hooks sub-command. - CmdHook = &cli.Command{ +func newHookCommand() *cli.Command { + return &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", Hidden: true, // internal commands shouldn't be visible Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), Commands: []*cli.Command{ - subcmdHookPreReceive, - subcmdHookUpdate, - subcmdHookPostReceive, - subcmdHookProcReceive, + newHookPreReceiveCommand(), + newHookUpdateCommand(), + newHookPostReceiveCommand(), + newHookProcReceiveCommand(), }, } +} - subcmdHookPreReceive = &cli.Command{ +func newHookPreReceiveCommand() *cli.Command { + return &cli.Command{ Name: "pre-receive", Usage: "Delegate pre-receive Git hook", Description: "This command should only be called by Git", @@ -55,7 +56,10 @@ var ( }, }, } - subcmdHookUpdate = &cli.Command{ +} + +func newHookUpdateCommand() *cli.Command { + return &cli.Command{ Name: "update", Usage: "Delegate update Git hook", Description: "This command should only be called by Git", @@ -66,7 +70,10 @@ var ( }, }, } - subcmdHookPostReceive = &cli.Command{ +} + +func newHookPostReceiveCommand() *cli.Command { + return &cli.Command{ Name: "post-receive", Usage: "Delegate post-receive Git hook", Description: "This command should only be called by Git", @@ -77,8 +84,11 @@ var ( }, }, } - // Note: new hook since git 2.29 - subcmdHookProcReceive = &cli.Command{ +} + +// Note: new hook since git 2.29 +func newHookProcReceiveCommand() *cli.Command { + return &cli.Command{ Name: "proc-receive", Usage: "Delegate proc-receive Git hook", Description: "This command should only be called by Git", @@ -89,7 +99,7 @@ var ( }, }, } -) +} type delayWriter struct { internal io.Writer diff --git a/cmd/keys.go b/cmd/keys.go index 035d39bfb8..912cf25091 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -15,40 +15,42 @@ import ( "github.com/urfave/cli/v3" ) -// CmdKeys represents the available keys sub-command -var CmdKeys = &cli.Command{ - Name: "keys", - Usage: "(internal) Should only be called by SSH server", - Hidden: true, // internal commands shouldn't be visible - Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runKeys, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "expected", - Aliases: []string{"e"}, - Value: "git", - Usage: "Expected user for whom provide key commands", +// NewKeysCommand returns the internal SSH key lookup sub-command. +func NewKeysCommand() *cli.Command { + return &cli.Command{ + Name: "keys", + Usage: "(internal) Should only be called by SSH server", + Hidden: true, // internal commands shouldn't be visible + Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runKeys, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "expected", + Aliases: []string{"e"}, + Value: "git", + Usage: "Expected user for whom provide key commands", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "Username trying to log in by SSH", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", + }, + &cli.StringFlag{ + Name: "content", + Aliases: []string{"k"}, + Value: "", + Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "Username trying to log in by SSH", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", - }, - &cli.StringFlag{ - Name: "content", - Aliases: []string{"k"}, - Value: "", - Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", - }, - }, + } } func runKeys(ctx context.Context, c *cli.Command) error { diff --git a/cmd/main.go b/cmd/main.go index 2ee00382d7..a6b89a6fad 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -112,35 +112,36 @@ func NewMainApp(appVer AppVersion) *cli.Command { Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, } + webCmd := newWebCommand() // these sub-commands need to use a config file subCmdWithConfig := []*cli.Command{ - CmdWeb, - CmdServ, - CmdHook, - CmdKeys, - CmdDump, - CmdAdmin, - CmdMigrate, - CmdDoctor, - CmdManager, - CmdEmbedded, - CmdMigrateStorage, - CmdDumpRepository, - CmdRestoreRepository, - CmdActions, + webCmd, + newServCommand(), + newHookCommand(), + NewKeysCommand(), + newDumpCommand(), + newAdminCommand(), + newMigrateCommand(), + newDoctorCommand(), + newManagerCommand(), + newEmbeddedCommand(), + newMigrateStorageCommand(), + newDumpRepositoryCommand(), + newRestoreRepositoryCommand(), + newActionsCommand(), } // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ cmdConfig(), cmdCert(), - CmdGenerate, - CmdDocs, + newGenerateCommand(), + newDocsCommand(), } // TODO: we should eventually drop the default command, // but not sure whether it would break Windows users who used to double-click the EXE to run. - app.DefaultCommand = CmdWeb.Name + app.DefaultCommand = webCmd.Name app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { diff --git a/cmd/manager.go b/cmd/manager.go index f0935ea065..586c65990b 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -13,22 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdManager represents the manager command - CmdManager = &cli.Command{ +func newManagerCommand() *cli.Command { + return &cli.Command{ Name: "manager", Usage: "Manage the running gitea process", Description: "This is a command for managing the running gitea process", Commands: []*cli.Command{ - subcmdShutdown, - subcmdRestart, - subcmdReloadTemplates, - subcmdFlushQueues, - subcmdLogging, - subCmdProcesses, + newShutdownCommand(), + newRestartCommand(), + newReloadTemplatesCommand(), + newFlushQueuesCommand(), + newLoggingCommand(), + newProcessesCommand(), }, } - subcmdShutdown = &cli.Command{ +} + +func newShutdownCommand() *cli.Command { + return &cli.Command{ Name: "shutdown", Usage: "Gracefully shutdown the running process", Flags: []cli.Flag{ @@ -38,7 +40,10 @@ var ( }, Action: runShutdown, } - subcmdRestart = &cli.Command{ +} + +func newRestartCommand() *cli.Command { + return &cli.Command{ Name: "restart", Usage: "Gracefully restart the running process - (not implemented for windows servers)", Flags: []cli.Flag{ @@ -48,7 +53,10 @@ var ( }, Action: runRestart, } - subcmdReloadTemplates = &cli.Command{ +} + +func newReloadTemplatesCommand() *cli.Command { + return &cli.Command{ Name: "reload-templates", Usage: "Reload template files in the running process", Flags: []cli.Flag{ @@ -58,7 +66,10 @@ var ( }, Action: runReloadTemplates, } - subcmdFlushQueues = &cli.Command{ +} + +func newFlushQueuesCommand() *cli.Command { + return &cli.Command{ Name: "flush-queues", Usage: "Flush queues in the running process", Action: runFlushQueues, @@ -77,7 +88,10 @@ var ( }, }, } - subCmdProcesses = &cli.Command{ +} + +func newProcessesCommand() *cli.Command { + return &cli.Command{ Name: "processes", Usage: "Display running processes within the current process", Action: runProcesses, @@ -107,7 +121,7 @@ var ( }, }, } -) +} func runShutdown(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index ac29e7d3e5..5812e707e2 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -15,8 +15,8 @@ import ( "github.com/urfave/cli/v3" ) -var ( - defaultLoggingFlags = []cli.Flag{ +func defaultLoggingFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "logger", Usage: `Logger name - will default to "default"`, @@ -57,8 +57,10 @@ var ( Name: "debug", }, } +} - subcmdLogging = &cli.Command{ +func newLoggingCommand() *cli.Command { + return &cli.Command{ Name: "logging", Usage: "Adjust logging commands", Commands: []*cli.Command{ @@ -109,7 +111,7 @@ var ( { Name: "file", Usage: "Add a file logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.StringFlag{ Name: "filename", Aliases: []string{"f"}, @@ -150,7 +152,7 @@ var ( }, { Name: "conn", Usage: "Add a net conn logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.BoolFlag{ Name: "reconnect-on-message", Aliases: []string{"R"}, @@ -191,7 +193,7 @@ var ( }, }, } -) +} func runRemoveLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/cmd/migrate.go b/cmd/migrate.go index e24dc9e572..016f8a0db5 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrate represents the available migrate sub-command. -var CmdMigrate = &cli.Command{ - Name: "migrate", - Usage: "Migrate the database", - Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, - Action: runMigrate, +func newMigrateCommand() *cli.Command { + return &cli.Command{ + Name: "migrate", + Usage: "Migrate the database", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, + Action: runMigrate, + } } func runMigrate(ctx context.Context, c *cli.Command) error { diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index a6bf9fa4b5..c9b82055c1 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -25,107 +25,108 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrateStorage represents the available migrate storage sub-command. -var CmdMigrateStorage = &cli.Command{ - Name: "migrate-storage", - Usage: "Migrate the storage", - Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", - Action: runMigrateStorage, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", +func newMigrateStorageCommand() *cli.Command { + return &cli.Command{ + Name: "migrate-storage", + Usage: "Migrate the storage", + Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", + Action: runMigrateStorage, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", + }, + &cli.StringFlag{ + Name: "storage", + Aliases: []string{"s"}, + Value: "", + Usage: "New storage type: local (default), minio or azureblob", + }, + &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Value: "", + Usage: "New storage placement if store is local (leave blank for default)", + }, + // Minio Storage special configurations + &cli.StringFlag{ + Name: "minio-endpoint", + Value: "", + Usage: "Minio storage endpoint", + }, + &cli.StringFlag{ + Name: "minio-access-key-id", + Value: "", + Usage: "Minio storage accessKeyID", + }, + &cli.StringFlag{ + Name: "minio-secret-access-key", + Value: "", + Usage: "Minio storage secretAccessKey", + }, + &cli.StringFlag{ + Name: "minio-bucket", + Value: "", + Usage: "Minio storage bucket", + }, + &cli.StringFlag{ + Name: "minio-location", + Value: "", + Usage: "Minio storage location to create bucket", + }, + &cli.StringFlag{ + Name: "minio-base-path", + Value: "", + Usage: "Minio storage base path on the bucket", + }, + &cli.BoolFlag{ + Name: "minio-use-ssl", + Usage: "Enable SSL for minio", + }, + &cli.BoolFlag{ + Name: "minio-insecure-skip-verify", + Usage: "Skip SSL verification", + }, + &cli.StringFlag{ + Name: "minio-checksum-algorithm", + Value: "", + Usage: "Minio checksum algorithm (default/md5)", + }, + &cli.StringFlag{ + Name: "minio-bucket-lookup-type", + Value: "", + Usage: "Minio bucket lookup type", + }, + // Azure Blob Storage special configurations + &cli.StringFlag{ + Name: "azureblob-endpoint", + Value: "", + Usage: "Azure Blob storage endpoint", + }, + &cli.StringFlag{ + Name: "azureblob-account-name", + Value: "", + Usage: "Azure Blob storage account name", + }, + &cli.StringFlag{ + Name: "azureblob-account-key", + Value: "", + Usage: "Azure Blob storage account key", + }, + &cli.StringFlag{ + Name: "azureblob-container", + Value: "", + Usage: "Azure Blob storage container", + }, + &cli.StringFlag{ + Name: "azureblob-base-path", + Value: "", + Usage: "Azure Blob storage base path", + }, }, - &cli.StringFlag{ - Name: "storage", - Aliases: []string{"s"}, - Value: "", - Usage: "New storage type: local (default), minio or azureblob", - }, - &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Value: "", - Usage: "New storage placement if store is local (leave blank for default)", - }, - // Minio Storage special configurations - &cli.StringFlag{ - Name: "minio-endpoint", - Value: "", - Usage: "Minio storage endpoint", - }, - &cli.StringFlag{ - Name: "minio-access-key-id", - Value: "", - Usage: "Minio storage accessKeyID", - }, - &cli.StringFlag{ - Name: "minio-secret-access-key", - Value: "", - Usage: "Minio storage secretAccessKey", - }, - &cli.StringFlag{ - Name: "minio-bucket", - Value: "", - Usage: "Minio storage bucket", - }, - &cli.StringFlag{ - Name: "minio-location", - Value: "", - Usage: "Minio storage location to create bucket", - }, - &cli.StringFlag{ - Name: "minio-base-path", - Value: "", - Usage: "Minio storage base path on the bucket", - }, - &cli.BoolFlag{ - Name: "minio-use-ssl", - Usage: "Enable SSL for minio", - }, - &cli.BoolFlag{ - Name: "minio-insecure-skip-verify", - Usage: "Skip SSL verification", - }, - &cli.StringFlag{ - Name: "minio-checksum-algorithm", - Value: "", - Usage: "Minio checksum algorithm (default/md5)", - }, - &cli.StringFlag{ - Name: "minio-bucket-lookup-type", - Value: "", - Usage: "Minio bucket lookup type", - }, - // Azure Blob Storage special configurations - &cli.StringFlag{ - Name: "azureblob-endpoint", - Value: "", - Usage: "Azure Blob storage endpoint", - }, - &cli.StringFlag{ - Name: "azureblob-account-name", - Value: "", - Usage: "Azure Blob storage account name", - }, - &cli.StringFlag{ - Name: "azureblob-account-key", - Value: "", - Usage: "Azure Blob storage account key", - }, - &cli.StringFlag{ - Name: "azureblob-container", - Value: "", - Usage: "Azure Blob storage container", - }, - &cli.StringFlag{ - Name: "azureblob-base-path", - Value: "", - Usage: "Azure Blob storage base path", - }, - }, + } } func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error { diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index c61f5a582e..26b4682f13 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -13,40 +13,41 @@ import ( "github.com/urfave/cli/v3" ) -// CmdRestoreRepository represents the available restore a repository sub-command. -var CmdRestoreRepository = &cli.Command{ - Name: "restore-repo", - Usage: "Restore the repository from disk", - Description: "This is a command for restoring the repository data.", - Action: runRestoreRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to restore from", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "Restore destination owner name", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "Restore destination repository name", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be restored, one or more units should be separated as comma. +func newRestoreRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "restore-repo", + Usage: "Restore the repository from disk", + Description: "This is a command for restoring the repository data.", + Action: runRestoreRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to restore from", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "Restore destination owner name", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "Restore destination repository name", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, + &cli.BoolFlag{ + Name: "validation", + Usage: "Sanity check the content of the files before trying to load them", + }, }, - &cli.BoolFlag{ - Name: "validation", - Usage: "Sanity check the content of the files before trying to load them", - }, - }, + } } func runRestoreRepository(ctx context.Context, c *cli.Command) error { diff --git a/cmd/serv.go b/cmd/serv.go index 4110fda0d5..a35d476c86 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -35,22 +35,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdServ represents the available serv sub-command. -var CmdServ = &cli.Command{ - Name: "serv", - Usage: "(internal) Should only be called by SSH shell", - Description: "Serv provides access auth for repositories", - Hidden: true, // Internal commands shouldn't be visible in help - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runServ, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "enable-pprof", +func newServCommand() *cli.Command { + return &cli.Command{ + Name: "serv", + Usage: "(internal) Should only be called by SSH shell", + Description: "Serv provides access auth for repositories", + Hidden: true, // Internal commands shouldn't be visible in help + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runServ, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "enable-pprof", + }, + &cli.BoolFlag{ + Name: "debug", + }, }, - &cli.BoolFlag{ - Name: "debug", - }, - }, + } } func setup(ctx context.Context, debug bool) { diff --git a/cmd/web.go b/cmd/web.go index 5000e780c5..994c481fc0 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -34,42 +34,43 @@ import ( // PIDFile could be set from build tag var PIDFile = "/run/gitea.pid" -// CmdWeb represents the available web sub-command. -var CmdWeb = &cli.Command{ - Name: "web", - Usage: "Start Gitea web server", - Description: `Gitea web server is the only thing you need to run, +func newWebCommand() *cli.Command { + return &cli.Command{ + Name: "web", + Usage: "Start Gitea web server", + Description: `Gitea web server is the only thing you need to run, and it takes care of all the other things for you`, - Before: PrepareConsoleLoggerLevel(log.INFO), - Action: runWeb, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: "3000", - Usage: "Temporary port number to prevent conflict", + Before: PrepareConsoleLoggerLevel(log.INFO), + Action: runWeb, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: "3000", + Usage: "Temporary port number to prevent conflict", + }, + &cli.StringFlag{ + Name: "install-port", + Value: "3000", + Usage: "Temporary port number to run the install page on to prevent conflict", + }, + &cli.StringFlag{ + Name: "pid", + Aliases: []string{"P"}, + Value: PIDFile, + Usage: "Custom pid file path", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display Fatal logging errors until logging is set-up", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Set initial logging to TRACE level until logging is properly set-up", + }, }, - &cli.StringFlag{ - Name: "install-port", - Value: "3000", - Usage: "Temporary port number to run the install page on to prevent conflict", - }, - &cli.StringFlag{ - Name: "pid", - Aliases: []string{"P"}, - Value: PIDFile, - Usage: "Custom pid file path", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display Fatal logging errors until logging is set-up", - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Set initial logging to TRACE level until logging is properly set-up", - }, - }, + } } func runHTTPRedirector() { diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go index d911bdf17d..b71d023f72 100644 --- a/tests/integration/cmd_keys_test.go +++ b/tests/integration/cmd_keys_test.go @@ -10,7 +10,6 @@ import ( "code.gitea.io/gitea/cmd" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -38,13 +37,14 @@ func Test_CmdKeys(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // FIXME: this test is not quite right. Each "command run" always re-initializes settings - defer test.MockVariableValue(&cmd.CmdKeys.Before, nil)() // don't re-initialize logger during the test + keysCmd := cmd.NewKeysCommand() + keysCmd.Before = nil // don't re-initialize logger during the test var stdout, stderr bytes.Buffer app := &cli.Command{ Writer: &stdout, ErrWriter: &stderr, - Commands: []*cli.Command{cmd.CmdKeys}, + Commands: []*cli.Command{keysCmd}, } err := app.Run(t.Context(), append([]string{"prog"}, tt.args...)) if tt.wantErr { From bc5c55407234c1c4cbc213fe4ae08083d1000d1e Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Wed, 25 Mar 2026 17:37:48 +0100 Subject: [PATCH 118/207] Feature non-zipped actions artifacts (action v7) (#36786) - content_encoding contains a slash => v4 artifact - updated proto files to support mime_type and no longer return errors for upload-artifact v7 - json and txt files are now previewed in browser - normalized content-disposition header creation - azure blob storage uploads directly in servedirect mode (no proxying data) - normalize content-disposition headers based on go mime package - getting both filename and filename* encoding is done via custom code Closes #36829 ----- Signed-off-by: ChristopherHX Co-authored-by: wxiaoguang --- models/actions/artifact.go | 38 +- models/fixtures/action_artifact.yml | 36 ++ modules/actions/artifacts.go | 51 +- modules/httplib/content_disposition.go | 65 ++ modules/httplib/content_disposition_test.go | 64 ++ modules/httplib/serve.go | 134 ++-- modules/httplib/serve_test.go | 8 +- modules/lfs/content_store.go | 2 +- modules/storage/minio.go | 6 +- modules/storage/storage.go | 24 +- modules/storage/storage_test.go | 40 +- modules/typesniffer/typesniffer.go | 4 + routers/api/actions/artifact.pb.go | 607 ++++++------------ routers/api/actions/artifact.proto | 3 + routers/api/actions/artifacts.go | 4 +- routers/api/actions/artifacts_chunks.go | 20 +- routers/api/actions/artifactsv4.go | 183 ++++-- routers/api/v1/repo/action.go | 18 +- routers/api/v1/repo/file.go | 39 +- routers/common/actions.go | 6 +- routers/common/serve.go | 29 +- routers/web/admin/diagnosis.go | 8 +- routers/web/repo/actions/view.go | 45 +- routers/web/repo/attachment.go | 4 +- routers/web/repo/download.go | 33 +- services/context/base.go | 4 +- services/lfs/server.go | 2 +- services/repository/archiver/archiver.go | 2 +- .../api_actions_artifact_v4_test.go | 350 +++++++--- 29 files changed, 1003 insertions(+), 826 deletions(-) create mode 100644 modules/httplib/content_disposition.go create mode 100644 modules/httplib/content_disposition_test.go diff --git a/models/actions/artifact.go b/models/actions/artifact.go index ec5cc0e32f..d61afb2aed 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -53,6 +53,11 @@ func init() { db.RegisterModel(new(ActionArtifact)) } +const ( + ContentEncodingV3Gzip = "gzip" + ContentTypeZip = "application/zip" +) + // ActionArtifact is a file that is stored in the artifact storage. type ActionArtifact struct { ID int64 `xorm:"pk autoincr"` @@ -61,16 +66,26 @@ type ActionArtifact struct { RepoID int64 `xorm:"index"` OwnerID int64 CommitSHA string - StoragePath string // The path to the artifact in the storage - FileSize int64 // The size of the artifact in bytes - FileCompressedSize int64 // The size of the artifact in bytes after gzip compression - ContentEncoding string // The content encoding of the artifact - ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it - ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it - Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete - CreatedUnix timeutil.TimeStamp `xorm:"created"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` - ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired + StoragePath string // The path to the artifact in the storage + FileSize int64 // The size of the artifact in bytes + FileCompressedSize int64 // The size of the artifact in bytes after gzip compression + + // The content encoding or content type of the artifact + // * empty or null: legacy (v3) uncompressed content + // * magic string "gzip" (ContentEncodingV3Gzip): v3 gzip compressed content + // * requires gzip decoding before storing in a zip for download + // * requires gzip content-encoding header when downloaded single files within a workflow + // * mime type for "Content-Type": + // * "application/zip" (ContentTypeZip), seems to be an abuse, fortunately there is no conflict, and it won't cause problems? + // * "application/pdf", "text/html", etc.: real content type of the artifact + ContentEncodingOrType string `xorm:"content_encoding"` + + ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it + ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it + Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired } func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { @@ -156,7 +171,8 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond { } if opts.FinalizedArtifactsV4 { cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired})) - cond = cond.And(builder.Eq{"content_encoding": "application/zip"}) + // see the comment of ActionArtifact.ContentEncodingOrType: "*/*" means the field is a content type + cond = cond.And(builder.Like{"content_encoding", "%/%"}) } return cond diff --git a/models/fixtures/action_artifact.yml b/models/fixtures/action_artifact.yml index ee8ef0d5ce..a25dfc205c 100644 --- a/models/fixtures/action_artifact.yml +++ b/models/fixtures/action_artifact.yml @@ -141,3 +141,39 @@ created_unix: 1730330775 updated_unix: 1730330775 expired_unix: 1738106775 + +- + id: 26 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/pdf" + artifact_path: "report.pdf" + artifact_name: "report.pdf" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +- + id: 27 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/html" + artifact_path: "report.html" + artifact_name: "report.html" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 diff --git a/modules/actions/artifacts.go b/modules/actions/artifacts.go index e8bf70ec31..4884eb42e8 100644 --- a/modules/actions/artifacts.go +++ b/modules/actions/artifacts.go @@ -5,44 +5,61 @@ package actions import ( "net/http" + "strings" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/services/context" ) -// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend -// The v4 backend ensures ContentEncoding is set to "application/zip", which is not the case for the old backend +// IsArtifactV4 detects whether the artifact is likely from v4. +// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash +// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend. func IsArtifactV4(art *actions_model.ActionArtifact) bool { - return art.ArtifactName+".zip" == art.ArtifactPath && art.ContentEncoding == "application/zip" + return strings.Contains(art.ContentEncodingOrType, "/") } -func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) { - if setting.Actions.ArtifactStorage.ServeDirect() { - u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, ctx.Req.Method, nil) - if u != nil && err == nil { - ctx.Redirect(u.String(), http.StatusFound) - return true, nil - } +func GetArtifactV4ServeDirectURL(art *actions_model.ActionArtifact, method string) (string, error) { + contentType := art.ContentEncodingOrType + u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, method, &storage.ServeDirectOptions{ContentType: contentType}) + if err != nil { + return "", err } - return false, nil + return u.String(), nil } -func DownloadArtifactV4Fallback(ctx *context.Base, art *actions_model.ActionArtifact) error { +func DownloadArtifactV4ServeDirect(ctx *context.Base, art *actions_model.ActionArtifact) bool { + if !setting.Actions.ArtifactStorage.ServeDirect() { + return false + } + u, err := GetArtifactV4ServeDirectURL(art, ctx.Req.Method) + if err != nil { + log.Error("GetArtifactV4ServeDirectURL: %v", err) + return false + } + ctx.Redirect(u, http.StatusFound) + return true +} + +func DownloadArtifactV4ReadStorage(ctx *context.Base, art *actions_model.ActionArtifact) error { f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { return err } defer f.Close() - http.ServeContent(ctx.Resp, ctx.Req, art.ArtifactName+".zip", art.CreatedUnix.AsLocalTime(), f) + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, f, httplib.ServeHeaderOptions{ + Filename: art.ArtifactPath, + ContentType: art.ContentEncodingOrType, // v4 guarantees that the field is Content-Type + }) return nil } func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error { - ok, err := DownloadArtifactV4ServeDirectOnly(ctx, art) - if ok || err != nil { - return err + if DownloadArtifactV4ServeDirect(ctx, art) { + return nil } - return DownloadArtifactV4Fallback(ctx, art) + return DownloadArtifactV4ReadStorage(ctx, art) } diff --git a/modules/httplib/content_disposition.go b/modules/httplib/content_disposition.go new file mode 100644 index 0000000000..da23dae221 --- /dev/null +++ b/modules/httplib/content_disposition.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +type ContentDispositionType string + +const ( + ContentDispositionInline ContentDispositionType = "inline" + ContentDispositionAttachment ContentDispositionType = "attachment" +) + +func needsEncodingRune(b rune) bool { + return (b < ' ' || b > '~') && b != '\t' +} + +// getSafeName replaces all invalid chars in the filename field by underscore +func getSafeName(s string) (_ string, needsEncoding bool) { + var out strings.Builder + for _, b := range s { + if needsEncodingRune(b) { + needsEncoding = true + out.WriteRune('_') + } else { + out.WriteRune(b) + } + } + return out.String(), needsEncoding +} + +func EncodeContentDispositionAttachment(filename string) string { + return encodeContentDisposition(ContentDispositionAttachment, filename) +} + +func EncodeContentDispositionInline(filename string) string { + return encodeContentDisposition(ContentDispositionInline, filename) +} + +// encodeContentDisposition encodes a correct Content-Disposition Header +func encodeContentDisposition(t ContentDispositionType, filename string) string { + safeFilename, needsEncoding := getSafeName(filename) + result := mime.FormatMediaType(string(t), map[string]string{"filename": safeFilename}) + // No need for the utf8 encoding + if !needsEncoding { + return result + } + utf8Result := mime.FormatMediaType(string(t), map[string]string{"filename": filename}) + + // The mime package might have unexpected results in other go versions + // Make tests instance fail, otherwise use the default behavior of the go mime package + if !strings.HasPrefix(result, string(t)+"; filename=") || !strings.HasPrefix(utf8Result, string(t)+"; filename*=") { + setting.PanicInDevOrTesting("Unexpected mime package result %s", result) + return utf8Result + } + + encodedFileName := strings.TrimPrefix(utf8Result, string(t)) + return result + encodedFileName +} diff --git a/modules/httplib/content_disposition_test.go b/modules/httplib/content_disposition_test.go new file mode 100644 index 0000000000..bf5040e107 --- /dev/null +++ b/modules/httplib/content_disposition_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestContentDisposition(t *testing.T) { + type testEntry struct { + disposition ContentDispositionType + filename string + header string + } + table := []testEntry{ + {disposition: ContentDispositionInline, filename: "test.txt", header: "inline; filename=test.txt"}, + {disposition: ContentDispositionInline, filename: "test❌.txt", header: "inline; filename=test_.txt; filename*=utf-8''test%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "test ❌.txt", header: "inline; filename=\"test _.txt\"; filename*=utf-8''test%20%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "\"test.txt", header: "inline; filename=\"\\\"test.txt\""}, + {disposition: ContentDispositionInline, filename: "hello\tworld.txt", header: "inline; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\tworld.txt", header: "attachment; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\nworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Aworld.txt"}, + {disposition: ContentDispositionAttachment, filename: "hello\rworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Dworld.txt"}, + } + + // Check the needsEncodingRune replacer ranges except tab that is checked above + // Any change in behavior should fail here + for c := ' '; !needsEncodingRune(c); c++ { + var header string + switch { + case strings.ContainsAny(string(c), ` (),/:;<=>?@[]`): + header = "inline; filename=\"hello" + string(c) + "world.txt\"" + case strings.ContainsAny(string(c), `"\`): + // This document advises against for backslash in quoted form: + // https://datatracker.ietf.org/doc/html/rfc6266#appendix-D + // However the mime package is not generating the filename* in this scenario + header = "inline; filename=\"hello\\" + string(c) + "world.txt\"" + default: + header = "inline; filename=hello" + string(c) + "world.txt" + } + table = append(table, testEntry{ + disposition: ContentDispositionInline, + filename: "hello" + string(c) + "world.txt", + header: header, + }) + } + + for _, entry := range table { + t.Run(string(entry.disposition)+"_"+entry.filename, func(t *testing.T) { + encoded := encodeContentDisposition(entry.disposition, entry.filename) + assert.Equal(t, entry.header, encoded) + disposition, params, err := mime.ParseMediaType(encoded) + require.NoError(t, err) + assert.Equal(t, string(entry.disposition), disposition) + assert.Equal(t, entry.filename, params["filename"]) + }) + } +} diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index fc7edc36c4..e8299d1c80 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -8,10 +8,9 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" - "net/url" "path" - "path/filepath" "strconv" "strings" "time" @@ -27,18 +26,19 @@ import ( ) type ServeHeaderOptions struct { - ContentType string // defaults to "application/octet-stream" - ContentTypeCharset string - ContentLength *int64 - Disposition string // defaults to "attachment" + ContentType string // defaults to "application/octet-stream" + ContentLength *int64 + Filename string - CacheIsPublic bool - CacheDuration time.Duration // defaults to 5 minutes - LastModified time.Time + ContentDisposition ContentDispositionType + + CacheIsPublic bool + CacheDuration time.Duration // defaults to 5 minutes + LastModified time.Time } // ServeSetHeaders sets necessary content serve headers -func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { +func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) { header := w.Header() skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp") @@ -46,14 +46,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { w.Header().Add(gzhttp.HeaderNoCompression, "1") } - contentType := typesniffer.MimeTypeApplicationOctetStream - if opts.ContentType != "" { - if opts.ContentTypeCharset != "" { - contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) - } else { - contentType = opts.ContentType - } - } + contentType := util.IfZero(opts.ContentType, typesniffer.MimeTypeApplicationOctetStream) header.Set("Content-Type", contentType) header.Set("X-Content-Type-Options", "nosniff") @@ -61,14 +54,18 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) } - if opts.Filename != "" { - disposition := opts.Disposition - if disposition == "" { - disposition = "attachment" - } + // Disable script execution of HTML/SVG files, since we serve the file from the same origin as Gitea server + header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") + if strings.Contains(contentType, "application/pdf") { + // no sandbox attribute for PDF as it breaks rendering in at least safari. this + // should generally be safe as scripts inside PDF can not escape the PDF document + // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context + header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") + } - backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \" - header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename))) + if opts.Filename != "" && opts.ContentDisposition != "" { + header.Set("Content-Disposition", encodeContentDisposition(opts.ContentDisposition, path.Base(opts.Filename))) header.Set("Access-Control-Expose-Headers", "Content-Disposition") } @@ -84,49 +81,40 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { } } -// ServeData download file from io.Reader -func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byte, opts *ServeHeaderOptions) { - // do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests - sniffedType := typesniffer.DetectContentType(mineBuf) - - // the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later - isPlain := sniffedType.IsText() || r.FormValue("render") != "" +func serveSetHeadersByUserContent(w http.ResponseWriter, contentPrefetchBuf []byte, opts ServeHeaderOptions) { + var detectCharset bool if setting.MimeTypeMap.Enabled { - fileExtension := strings.ToLower(filepath.Ext(opts.Filename)) + fileExtension := strings.ToLower(path.Ext(opts.Filename)) opts.ContentType = setting.MimeTypeMap.Map[fileExtension] + detectCharset = !strings.Contains(opts.ContentType, "charset=") } if opts.ContentType == "" { + sniffedType := typesniffer.DetectContentType(contentPrefetchBuf) if sniffedType.IsBrowsableBinaryType() { opts.ContentType = sniffedType.GetMimeType() - } else if isPlain { + } else if sniffedType.IsText() { + // intentionally do not render user's HTML content as a page, for safety, and avoid content spamming & abusing opts.ContentType = "text/plain" + detectCharset = true } else { opts.ContentType = typesniffer.MimeTypeApplicationOctetStream } } - if isPlain { - charset, _ := charsetModule.DetectEncoding(mineBuf) - opts.ContentTypeCharset = strings.ToLower(charset) + if detectCharset { + if charset, _ := charsetModule.DetectEncoding(contentPrefetchBuf); charset != "" { + opts.ContentType += "; charset=" + strings.ToLower(charset) + } } - // serve types that can present a security risk with CSP - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") - - if sniffedType.IsPDF() { - // no sandbox attribute for PDF as it breaks rendering in at least safari. this - // should generally be safe as scripts inside PDF can not escape the PDF document - // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion - // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") - } - - // TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE - opts.Disposition = "inline" - if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled { - opts.Disposition = "attachment" + if opts.ContentDisposition == "" { + sniffedType := typesniffer.FromContentType(opts.ContentType) + opts.ContentDisposition = ContentDispositionInline + if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled { + opts.ContentDisposition = ContentDispositionAttachment + } } ServeSetHeaders(w, opts) @@ -134,7 +122,10 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt const mimeDetectionBufferLen = 1024 -func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts *ServeHeaderOptions) { +func ServeUserContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts ServeHeaderOptions) { + if opts.ContentLength != nil { + panic("do not set ContentLength, use size argument instead") + } buf := make([]byte, mimeDetectionBufferLen) n, err := util.ReadAtMost(reader, buf) if err != nil { @@ -144,7 +135,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re if n >= 0 { buf = buf[:n] } - setServeHeadersByFile(r, w, buf, opts) + serveSetHeadersByUserContent(w, buf, opts) // reset the reader to the beginning reader = io.MultiReader(bytes.NewReader(buf), reader) @@ -198,32 +189,29 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re partialLength := end - start + 1 w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10)) - if _, err = io.CopyN(io.Discard, reader, start); err != nil { - http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) - return + + if seeker, ok := reader.(io.Seeker); ok { + if _, err = seeker.Seek(start, io.SeekStart); err != nil { + http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) + return + } + } else { + if _, err = io.CopyN(io.Discard, reader, start); err != nil { + http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) + return + } } w.WriteHeader(http.StatusPartialContent) _, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error } -func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, modTime *time.Time, reader io.ReadSeeker, opts *ServeHeaderOptions) { - buf := make([]byte, mimeDetectionBufferLen) - n, err := util.ReadAtMost(reader, buf) +func ServeUserContentByFile(r *http.Request, w http.ResponseWriter, file fs.File, opts ServeHeaderOptions) { + info, err := file.Stat() if err != nil { - http.Error(w, "serve content: unable to read", http.StatusInternalServerError) + http.Error(w, "unable to serve file, stat error", http.StatusInternalServerError) return } - if _, err = reader.Seek(0, io.SeekStart); err != nil { - http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) - return - } - if n >= 0 { - buf = buf[:n] - } - setServeHeadersByFile(r, w, buf, opts) - if modTime == nil { - modTime = &time.Time{} - } - http.ServeContent(w, r, opts.Filename, *modTime, reader) + opts.LastModified = info.ModTime() + ServeUserContentByReader(r, w, info.Size(), file, opts) } diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go index 78b88c9b5f..38cf4c197f 100644 --- a/modules/httplib/serve_test.go +++ b/modules/httplib/serve_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestServeContentByReader(t *testing.T) { +func TestServeUserContentByReader(t *testing.T) { data := "0123456789abcdef" test := func(t *testing.T, expectedStatusCode int, expectedContent string) { @@ -27,7 +27,7 @@ func TestServeContentByReader(t *testing.T) { } reader := strings.NewReader(data) w := httptest.NewRecorder() - ServeContentByReader(r, w, int64(len(data)), reader, &ServeHeaderOptions{}) + ServeUserContentByReader(r, w, int64(len(data)), reader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) @@ -58,7 +58,7 @@ func TestServeContentByReader(t *testing.T) { }) } -func TestServeContentByReadSeeker(t *testing.T) { +func TestServeUserContentByFile(t *testing.T) { data := "0123456789abcdef" tmpFile := t.TempDir() + "/test" err := os.WriteFile(tmpFile, []byte(data), 0o644) @@ -76,7 +76,7 @@ func TestServeContentByReadSeeker(t *testing.T) { defer seekReader.Close() w := httptest.NewRecorder() - ServeContentByReadSeeker(r, w, nil, seekReader, &ServeHeaderOptions{}) + ServeUserContentByFile(r, w, seekReader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go index 0d9c0c98ac..be1e6c8e90 100644 --- a/modules/lfs/content_store.go +++ b/modules/lfs/content_store.go @@ -104,7 +104,7 @@ func (s *ContentStore) Verify(pointer Pointer) (bool, error) { } // ReadMetaObject will read a git_model.LFSMetaObject and return a reader -func ReadMetaObject(pointer Pointer) (io.ReadSeekCloser, error) { +func ReadMetaObject(pointer Pointer) (storage.Object, error) { contentStore := NewContentStore() return contentStore.Get(pointer) } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 1355280f36..ace78bb610 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -23,11 +23,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" ) -var ( - _ ObjectStorage = &MinioStorage{} - - quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") -) +var _ ObjectStorage = &MinioStorage{} type minioObject struct { *minio.Object diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 2491c77a3e..e19c421ba8 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -12,6 +12,7 @@ import ( "os" "path" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" @@ -62,31 +63,30 @@ type Object interface { type ServeDirectOptions struct { // Overrides the automatically detected MIME type. ContentType string - // Overrides the default Content-Disposition header, which is `inline; filename="name"`. - ContentDisposition string } // Safe defaults are applied only when not explicitly overridden by the caller. -func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret ServeDirectOptions) { +func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret struct { + ContentType string + ContentDisposition string +}, +) { // Here we might not know the real filename, and it's quite inefficient to detect the MIME type by pre-fetching the object head. // So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI. // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future if optsOptional != nil { - ret = *optsOptional + ret.ContentType = optsOptional.ContentType } - - // TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE + name = path.Base(name) if ret.ContentType == "" { ext := path.Ext(name) ret.ContentType = public.DetectWellKnownMimeType(ext) } - if ret.ContentDisposition == "" { - // When using ServeDirect, the URL is from the object storage's web server, - // it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly. - // If a browser doesn't support the content type to be displayed inline, browser will download with the filename. - ret.ContentDisposition = fmt.Sprintf(`inline; filename="%s"`, quoteEscaper.Replace(name)) - } + // When using ServeDirect, the URL is from the object storage's web server, + // it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly. + // If a browser doesn't support the content type to be displayed inline, browser will download with the filename. + ret.ContentDisposition = httplib.EncodeContentDispositionInline(name) return ret } diff --git a/modules/storage/storage_test.go b/modules/storage/storage_test.go index 4156723c36..83ee2ef793 100644 --- a/modules/storage/storage_test.go +++ b/modules/storage/storage_test.go @@ -53,7 +53,12 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { } } -func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected ServeDirectOptions, reqParams *ServeDirectOptions) { +type expectedServeDirectHeaders struct { + ContentType string + ContentDisposition string +} + +func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected expectedServeDirectHeaders, reqParams *ServeDirectOptions) { u, err := s.ServeDirectURL(path, name, http.MethodGet, reqParams) require.NoError(t, err) resp, err := http.Get(u.String()) @@ -71,36 +76,29 @@ func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg s, err := NewStorage(typStr, cfg) assert.NoError(t, err) - data := "Q2xTckt6Y1hDOWh0" // arbitrary test content; specific value is irrelevant to this test - testfilename := "test.txt" // arbitrary file name; specific value is irrelevant to this test - _, err = s.Save(testfilename, strings.NewReader(data), int64(len(data))) + testFilename := "test.txt" + _, err = s.Save(testFilename, strings.NewReader("dummy-content"), -1) assert.NoError(t, err) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{ + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.txt", expectedServeDirectHeaders{ ContentType: "text/plain; charset=utf-8", - ContentDisposition: `inline; filename="test.txt"`, + ContentDisposition: `inline; filename=test.txt`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.pdf", ServeDirectOptions{ + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.pdf", expectedServeDirectHeaders{ ContentType: "application/pdf", - ContentDisposition: `inline; filename="test.pdf"`, + ContentDisposition: `inline; filename=test.pdf`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{ - ContentDisposition: `inline; filename="test.wasm"`, + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentDisposition: `inline; filename=test.wasm`, }, nil) - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{ - ContentDisposition: `inline; filename="test.wasm"`, - }, &ServeDirectOptions{}) - - testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{ - ContentType: "application/octet-stream", - ContentDisposition: `inline; filename="test.xml"`, + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentType: "application/wasm", + ContentDisposition: `inline; filename=test.wasm`, }, &ServeDirectOptions{ - ContentType: "application/octet-stream", - ContentDisposition: `inline; filename="test.xml"`, + ContentType: "application/wasm", }) - - assert.NoError(t, s.Delete(testfilename)) + assert.NoError(t, s.Delete(testFilename)) } diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index 0c4867d8f0..90423d48ce 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -183,3 +183,7 @@ func DetectContentType(data []byte) SniffedType { } return SniffedType{ct} } + +func FromContentType(contentType string) SniffedType { + return SniffedType{contentType} +} diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go index 590eda9fb9..130e20301f 100644 --- a/routers/api/actions/artifact.pb.go +++ b/routers/api/actions/artifact.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v4.25.2 +// protoc-gen-go v1.36.11 +// protoc v7.34.0 // source: artifact.proto package actions @@ -12,6 +12,7 @@ package actions import ( reflect "reflect" sync "sync" + unsafe "unsafe" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -27,24 +28,22 @@ const ( ) type CreateArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` - Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` + MimeType *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateArtifactRequest) Reset() { *x = CreateArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateArtifactRequest) String() string { @@ -55,7 +54,7 @@ func (*CreateArtifactRequest) ProtoMessage() {} func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -105,22 +104,26 @@ func (x *CreateArtifactRequest) GetVersion() int32 { return 0 } -type CreateArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *CreateArtifactRequest) GetMimeType() *wrapperspb.StringValue { + if x != nil { + return x.MimeType + } + return nil +} - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` +type CreateArtifactResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateArtifactResponse) Reset() { *x = CreateArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateArtifactResponse) String() string { @@ -131,7 +134,7 @@ func (*CreateArtifactResponse) ProtoMessage() {} func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -161,24 +164,21 @@ func (x *CreateArtifactResponse) GetSignedUploadUrl() string { } type FinalizeArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FinalizeArtifactRequest) Reset() { *x = FinalizeArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeArtifactRequest) String() string { @@ -189,7 +189,7 @@ func (*FinalizeArtifactRequest) ProtoMessage() {} func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -240,21 +240,18 @@ func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue { } type FinalizeArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` unknownFields protoimpl.UnknownFields - - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *FinalizeArtifactResponse) Reset() { *x = FinalizeArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeArtifactResponse) String() string { @@ -265,7 +262,7 @@ func (*FinalizeArtifactResponse) ProtoMessage() {} func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -295,23 +292,20 @@ func (x *FinalizeArtifactResponse) GetArtifactId() int64 { } type ListArtifactsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"` IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListArtifactsRequest) Reset() { *x = ListArtifactsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsRequest) String() string { @@ -322,7 +316,7 @@ func (*ListArtifactsRequest) ProtoMessage() {} func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -366,20 +360,17 @@ func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value { } type ListArtifactsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` unknownFields protoimpl.UnknownFields - - Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListArtifactsResponse) Reset() { *x = ListArtifactsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsResponse) String() string { @@ -390,7 +381,7 @@ func (*ListArtifactsResponse) ProtoMessage() {} func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -413,25 +404,22 @@ func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_Monolith } type ListArtifactsResponse_MonolithArtifact struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"` Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListArtifactsResponse_MonolithArtifact) Reset() { *x = ListArtifactsResponse_MonolithArtifact{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListArtifactsResponse_MonolithArtifact) String() string { @@ -442,7 +430,7 @@ func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {} func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -500,22 +488,19 @@ func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Tim } type GetSignedArtifactURLRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSignedArtifactURLRequest) Reset() { *x = GetSignedArtifactURLRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetSignedArtifactURLRequest) String() string { @@ -526,7 +511,7 @@ func (*GetSignedArtifactURLRequest) ProtoMessage() {} func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -563,20 +548,17 @@ func (x *GetSignedArtifactURLRequest) GetName() string { } type GetSignedArtifactURLResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` unknownFields protoimpl.UnknownFields - - SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetSignedArtifactURLResponse) Reset() { *x = GetSignedArtifactURLResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetSignedArtifactURLResponse) String() string { @@ -587,7 +569,7 @@ func (*GetSignedArtifactURLResponse) ProtoMessage() {} func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -610,22 +592,19 @@ func (x *GetSignedArtifactURLResponse) GetSignedUrl() string { } type DeleteArtifactRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` - WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"` + WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteArtifactRequest) Reset() { *x = DeleteArtifactRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteArtifactRequest) String() string { @@ -636,7 +615,7 @@ func (*DeleteArtifactRequest) ProtoMessage() {} func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -673,21 +652,18 @@ func (x *DeleteArtifactRequest) GetName() string { } type DeleteArtifactResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` unknownFields protoimpl.UnknownFields - - Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` - ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteArtifactResponse) Reset() { *x = DeleteArtifactResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_artifact_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_artifact_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteArtifactResponse) String() string { @@ -698,7 +674,7 @@ func (*DeleteArtifactResponse) ProtoMessage() {} func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message { mi := &file_artifact_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -729,173 +705,105 @@ func (x *DeleteArtifactResponse) GetArtifactId() int64 { var File_artifact_proto protoreflect.FileDescriptor -var file_artifact_proto_rawDesc = []byte{ - 0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, - 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, - 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, - 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, - 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, - 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, - 0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c, - 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8, - 0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, - 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, - 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, - 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, - 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, - 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a, - 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, - 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26, - 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, - 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, - 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, - 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, - 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, - 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64, - 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, - 0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, - 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, - 0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, - 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, - 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, - 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53, - 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, - 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69, - 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, - 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, - 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, - 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, - 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, - 0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_artifact_proto_rawDesc = "" + + "\n" + + "\x0eartifact.proto\x12\x1dgithub.actions.results.api.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"\xb0\x02\n" + + "\x15CreateArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x129\n" + + "\n" + + "expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x18\n" + + "\aversion\x18\x05 \x01(\x05R\aversion\x129\n" + + "\tmime_type\x18\x06 \x01(\v2\x1c.google.protobuf.StringValueR\bmimeType\"T\n" + + "\x16CreateArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12*\n" + + "\x11signed_upload_url\x18\x02 \x01(\tR\x0fsignedUploadUrl\"\xe8\x01\n" + + "\x17FinalizeArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" + + "\x04size\x18\x04 \x01(\x03R\x04size\x120\n" + + "\x04hash\x18\x05 \x01(\v2\x1c.google.protobuf.StringValueR\x04hash\"K\n" + + "\x18FinalizeArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" + + "\vartifact_id\x18\x02 \x01(\x03R\n" + + "artifactId\"\x84\x02\n" + + "\x14ListArtifactsRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12=\n" + + "\vname_filter\x18\x03 \x01(\v2\x1c.google.protobuf.StringValueR\n" + + "nameFilter\x128\n" + + "\tid_filter\x18\x04 \x01(\v2\x1b.google.protobuf.Int64ValueR\bidFilter\"|\n" + + "\x15ListArtifactsResponse\x12c\n" + + "\tartifacts\x18\x01 \x03(\v2E.github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifactR\tartifacts\"\xa1\x02\n" + + "&ListArtifactsResponse_MonolithArtifact\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x1f\n" + + "\vdatabase_id\x18\x03 \x01(\x03R\n" + + "databaseId\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x12\n" + + "\x04size\x18\x05 \x01(\x03R\x04size\x129\n" + + "\n" + + "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xa6\x01\n" + + "\x1bGetSignedArtifactURLRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"=\n" + + "\x1cGetSignedArtifactURLResponse\x12\x1d\n" + + "\n" + + "signed_url\x18\x01 \x01(\tR\tsignedUrl\"\xa0\x01\n" + + "\x15DeleteArtifactRequest\x125\n" + + "\x17workflow_run_backend_id\x18\x01 \x01(\tR\x14workflowRunBackendId\x12<\n" + + "\x1bworkflow_job_run_backend_id\x18\x02 \x01(\tR\x17workflowJobRunBackendId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"I\n" + + "\x16DeleteArtifactResponse\x12\x0e\n" + + "\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" + + "\vartifact_id\x18\x02 \x01(\x03R\n" + + "artifactIdB)Z'code.gitea.io/gitea/routers/api/actionsb\x06proto3" var ( file_artifact_proto_rawDescOnce sync.Once - file_artifact_proto_rawDescData = file_artifact_proto_rawDesc + file_artifact_proto_rawDescData []byte ) func file_artifact_proto_rawDescGZIP() []byte { file_artifact_proto_rawDescOnce.Do(func() { - file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData) + file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc))) }) return file_artifact_proto_rawDescData } -var ( - file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) - file_artifact_proto_goTypes = []interface{}{ - (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest - (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse - (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest - (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse - (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest - (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse - (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact - (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest - (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse - (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest - (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp - (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue - (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value - } -) - +var file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_artifact_proto_goTypes = []any{ + (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest + (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse + (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest + (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse + (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest + (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse + (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest + (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse + (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest + (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue + (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value +} var file_artifact_proto_depIdxs = []int32{ 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp - 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue - 12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue - 13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value - 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact - 11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 12, // 1: github.actions.results.api.v1.CreateArtifactRequest.mime_type:type_name -> google.protobuf.StringValue + 12, // 2: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue + 12, // 3: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue + 13, // 4: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value + 6, // 5: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact + 11, // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_artifact_proto_init() } @@ -903,145 +811,11 @@ func file_artifact_proto_init() { if File_artifact_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListArtifactsResponse_MonolithArtifact); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSignedArtifactURLRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetSignedArtifactURLResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteArtifactRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteArtifactResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_artifact_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_artifact_proto_rawDesc), len(file_artifact_proto_rawDesc)), NumEnums: 0, NumMessages: 11, NumExtensions: 0, @@ -1052,7 +826,6 @@ func file_artifact_proto_init() { MessageInfos: file_artifact_proto_msgTypes, }.Build() File_artifact_proto = out.File - file_artifact_proto_rawDesc = nil file_artifact_proto_goTypes = nil file_artifact_proto_depIdxs = nil } diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto index c68e5d030d..7da8bad564 100644 --- a/routers/api/actions/artifact.proto +++ b/routers/api/actions/artifact.proto @@ -5,12 +5,15 @@ import "google/protobuf/wrappers.proto"; package github.actions.results.api.v1; +option go_package = "code.gitea.io/gitea/routers/api/actions"; + message CreateArtifactRequest { string workflow_run_backend_id = 1; string workflow_job_run_backend_id = 2; string name = 3; google.protobuf.Timestamp expires_at = 4; int32 version = 5; + google.protobuf.StringValue mime_type = 6; } message CreateArtifactResponse { diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 76facd769f..a6722616cf 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -282,7 +282,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { artifact.FileCompressedSize != chunksTotalSize { artifact.FileSize = fileRealTotalSize artifact.FileCompressedSize = chunksTotalSize - artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding") + artifact.ContentEncodingOrType = ctx.Req.Header.Get("Content-Encoding") if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error update artifact: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error update artifact") @@ -492,7 +492,7 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { defer fd.Close() // if artifact is compressed, set content-encoding header to gzip - if artifact.ContentEncoding == "gzip" { + if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip { ctx.Resp.Header().Set("Content-Encoding", "gzip") } log.Debug("[artifact] downloadArtifact, name: %s, path: %s, storage: %s, size: %d", artifact.ArtifactName, artifact.ArtifactPath, artifact.StoragePath, artifact.FileSize) diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 86a51d6ca6..8d04c68922 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -285,6 +285,17 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int return nil } +func generateArtifactStoragePath(artifact *actions.ActionArtifact) string { + // if chunk is gzip, use gz as extension + // download-artifact action will use content-encoding header to decide if it should decompress the file + extension := "chunk" + if artifact.ContentEncodingOrType == actions.ContentEncodingV3Gzip { + extension = "chunk.gz" + } + + return fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) +} + func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error { sort.Slice(chunks, func(i, j int) bool { return chunks[i].Start < chunks[j].Start @@ -335,15 +346,8 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st mergedReader = io.TeeReader(mergedReader, hashSha256) } - // if chunk is gzip, use gz as extension - // download-artifact action will use content-encoding header to decide if it should decompress the file - extension := "chunk" - if artifact.ContentEncoding == "gzip" { - extension = "chunk.gz" - } - // save merged file - storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) + storagePath := generateArtifactStoragePath(artifact) written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize) if err != nil { return fmt.Errorf("save merged file error: %v", err) diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 62605f2702..e86645cb0c 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -89,10 +89,12 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/xml" "errors" "fmt" "io" + "mime" "net/http" "net/url" "path" @@ -100,8 +102,9 @@ import ( "strings" "time" - "code.gitea.io/gitea/models/actions" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -113,12 +116,10 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/timestamppb" + "xorm.io/builder" ) -const ( - ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" - ArtifactV4ContentEncoding = "application/zip" -) +const ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService" type artifactV4Routes struct { prefix string @@ -219,7 +220,7 @@ func parseChunkFileItemV4(st storage.ObjectStorage, artifactID int64, fpath stri return &item, nil } -func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { +func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions_model.ActionTask, string, bool) { rawTaskID := ctx.Req.URL.Query().Get("taskID") rawArtifactID := ctx.Req.URL.Query().Get("artifactID") sig := ctx.Req.URL.Query().Get("sig") @@ -246,13 +247,13 @@ func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (* ctx.HTTPError(http.StatusUnauthorized, "Error link expired") return nil, "", false } - task, err := actions.GetTaskByID(ctx, taskID) + task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task by ID") return nil, "", false } - if task.Status != actions.StatusRunning { + if task.Status != actions_model.StatusRunning { log.Error("Error runner api getting task: task is not running") ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return nil, "", false @@ -265,9 +266,9 @@ func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (* return task, artifactName, true } -func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) { - var art actions.ActionArtifact - has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art) +func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions_model.ActionArtifact, error) { + var art actions_model.ActionArtifact + has, err := db.GetEngine(ctx).Where(builder.Eq{"run_id": runID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).Get(&art) if err != nil { return nil, err } else if !has { @@ -321,26 +322,59 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { if req.ExpiresAt != nil { retentionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24) } + encoding := req.GetMimeType().GetValue() + // Validate media type + if encoding != "" { + encoding, _, _ = mime.ParseMediaType(encoding) + } + fileName := artifactName + if !strings.Contains(encoding, "/") || strings.EqualFold(encoding, actions_model.ContentTypeZip) && !strings.HasSuffix(fileName, ".zip") { + encoding = actions_model.ContentTypeZip + fileName = artifactName + ".zip" + } // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", retentionDays) + artifact, err := actions_model.CreateArtifact(ctx, ctx.ActionTask, artifactName, fileName, retentionDays) if err != nil { log.Error("Error create or get artifact: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error create or get artifact") return } - artifact.ContentEncoding = ArtifactV4ContentEncoding + artifact.ContentEncodingOrType = encoding artifact.FileSize = 0 artifact.FileCompressedSize = 0 - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + + var respData CreateArtifactResponse + + if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType { + storagePath := generateArtifactStoragePath(artifact) + if artifact.StoragePath != "" { + _ = storage.ActionsArtifacts.Delete(artifact.StoragePath) + } + artifact.StoragePath = storagePath + artifact.Status = actions_model.ArtifactStatusUploadPending + u, err := storage.ActionsArtifacts.ServeDirectURL(artifact.StoragePath, artifact.ArtifactPath, http.MethodPut, nil) + if err != nil { + log.Error("Error ServeDirectURL: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error ServeDirectURL") + return + } + respData = CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: u.String(), + } + } else { + respData = CreateArtifactResponse{ + Ok: true, + SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), + } + } + + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return } - respData := CreateArtifactResponse{ - Ok: true, - SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), - } r.sendProtobufBody(ctx, &respData) } @@ -370,7 +404,7 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { } artifact.FileCompressedSize += uploadedLength artifact.FileSize += uploadedLength - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return @@ -448,9 +482,27 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { return } - var chunks []*chunkFileItem + if setting.Actions.ArtifactStorage.ServeDirect() && setting.Actions.ArtifactStorage.Type == setting.AzureBlobStorageType { + r.finalizeAzureServeDirect(ctx, &req, artifact) + } else { + r.finalizeDefaultArtifact(ctx, &req, artifact, runID) + } + + // Return on finalize error + if ctx.Written() { + return + } + + respData := FinalizeArtifactResponse{ + Ok: true, + ArtifactId: artifact.ID, + } + r.sendProtobufBody(ctx, &respData) +} + +func (r *artifactV4Routes) finalizeDefaultArtifact(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact, runID int64) { blockList, blockListErr := r.readBlockList(runID, artifact.ID) - chunks, err = listOrderedChunksForArtifact(r.fs, runID, artifact.ID, blockList) + chunks, err := listOrderedChunksForArtifact(r.fs, runID, artifact.ID, blockList) if err != nil { log.Error("Error list chunks: %v", errors.Join(blockListErr, err)) ctx.HTTPError(http.StatusInternalServerError, "Error list chunks") @@ -465,21 +517,63 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { return } - checksum := "" - if req.Hash != nil { - checksum = req.Hash.Value - } - if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil { + if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, req.GetHash().GetValue()); err != nil { log.Error("Error merge chunks: %v", err) ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } +} - respData := FinalizeArtifactResponse{ - Ok: true, - ArtifactId: artifact.ID, +func (r *artifactV4Routes) finalizeAzureServeDirect(ctx *ArtifactContext, req *FinalizeArtifactRequest, artifact *actions_model.ActionArtifact) { + checksumValue, hasSha256Checksum := strings.CutPrefix(req.GetHash().GetValue(), "sha256:") + var actualLength int64 + if hasSha256Checksum { + hashSha256 := sha256.New() + obj, err := storage.ActionsArtifacts.Open(artifact.StoragePath) + if err != nil { + log.Error("Error read block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error read block") + return + } + defer obj.Close() + actualLength, err = io.Copy(hashSha256, obj) + if err != nil { + log.Error("Error read block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error read block") + return + } + rawChecksum := hashSha256.Sum(nil) + actualChecksum := hex.EncodeToString(rawChecksum) + if checksumValue != actualChecksum { + log.Error("Error merge chunks: checksum mismatch") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: checksum mismatch") + return + } + } else { + fi, err := storage.ActionsArtifacts.Stat(artifact.StoragePath) + if err != nil { + log.Error("Error stat block: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error stat block") + return + } + actualLength = fi.Size() + } + + if req.Size != actualLength { + log.Error("Error merge chunks: length mismatch") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks: length mismatch") + return + } + + // Update artifact metadata and status now that the upload is confirmed. + artifact.FileSize = actualLength + artifact.FileCompressedSize = actualLength + artifact.Status = actions_model.ArtifactStatusUploadConfirmed + if err := actions_model.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") + return } - r.sendProtobufBody(ctx, &respData) } func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { @@ -493,9 +587,10 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ - RunID: runID, - Status: int(actions.ArtifactStatusUploadConfirmed), + artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ + RunID: runID, + Status: int(actions_model.ArtifactStatusUploadConfirmed), + FinalizedArtifactsV4: true, }) if err != nil { log.Error("Error getting artifacts: %v", err) @@ -507,7 +602,7 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { table := map[string]*ListArtifactsResponse_MonolithArtifact{} for _, artifact := range artifacts { - if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding { + if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value { table[artifact.ArtifactName] = nil continue } @@ -553,7 +648,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - if artifact.Status != actions.ArtifactStatusUploadConfirmed { + if artifact.Status != actions_model.ArtifactStatusUploadConfirmed { log.Error("Error artifact not found: %s", artifact.Status.ToString()) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return @@ -563,9 +658,9 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { if setting.Actions.ArtifactStorage.ServeDirect() { // DO NOT USE the http POST method coming from the getSignedArtifactURL endpoint - u, err := storage.ActionsArtifacts.ServeDirectURL(artifact.StoragePath, artifact.ArtifactPath, http.MethodGet, nil) - if u != nil && err == nil { - respData.SignedUrl = u.String() + u, err := actions.GetArtifactV4ServeDirectURL(artifact, http.MethodGet) + if err == nil { + respData.SignedUrl = u } } if respData.SignedUrl == "" { @@ -587,15 +682,17 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - if artifact.Status != actions.ArtifactStatusUploadConfirmed { + if artifact.Status != actions_model.ArtifactStatusUploadConfirmed { log.Error("Error artifact not found: %s", artifact.Status.ToString()) ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } - file, _ := r.fs.Open(artifact.StoragePath) - - _, _ = io.Copy(ctx.Resp, file) + err = actions.DownloadArtifactV4ReadStorage(ctx.Base, artifact) + if err != nil { + log.Error("Error serve artifact: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "failed to download artifact") + } } func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { @@ -617,7 +714,7 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { return } - err = actions.SetArtifactNeedDelete(ctx, runID, req.Name) + err = actions_model.SetArtifactNeedDelete(ctx, runID, req.Name) if err != nil { log.Error("Error deleting artifacts: %v", err) ctx.HTTPError(http.StatusInternalServerError, err.Error()) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d704092051..0c48f732ab 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1784,7 +1784,7 @@ func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) str func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string { // endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw" expires := time.Now().Add(60 * time.Minute).Unix() - uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) + uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.RawURLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) return uploadURL } @@ -1829,18 +1829,16 @@ func DownloadArtifact(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, "Artifact has expired") return } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) if actions.IsArtifactV4(art) { - ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art) - if ok { - return - } - if err != nil { - ctx.APIErrorInternal(err) + // @actions/toolkit asserts that downloaded artifacts of a different runid return 302 + // https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210 + if actions.DownloadArtifactV4ServeDirect(ctx.Base, art) { return } + // @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it + // TODO: a perma link to the code for reference redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID) ctx.Redirect(redirectURL, http.StatusFound) return @@ -1868,7 +1866,7 @@ func DownloadArtifactRaw(ctx *context.APIContext) { sigStr := ctx.Req.URL.Query().Get("sig") expiresStr := ctx.Req.URL.Query().Get("expires") - sigBytes, _ := base64.URLEncoding.DecodeString(sigStr) + sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr) expires, _ := strconv.ParseInt(expiresStr, 10, 64) expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID) @@ -1887,8 +1885,6 @@ func DownloadArtifactRaw(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, "Artifact has expired") return } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) - if actions.IsArtifactV4(art) { err := actions.DownloadArtifactV4(ctx.Base, art) if err != nil { diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index d0596d778b..9949928622 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -17,9 +17,9 @@ import ( git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" @@ -151,35 +151,18 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes, // we can simply read this in one go (This saves reading it twice) - dataRc, err := blob.DataAsync() + lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) if err != nil { ctx.APIErrorInternal(err) return } - buf, err := io.ReadAll(dataRc) - if err != nil { - _ = dataRc.Close() - ctx.APIErrorInternal(err) - return - } - - if err := dataRc.Close(); err != nil { - log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err) - } - // Check if the blob represents a pointer - pointer, _ := lfs.ReadPointer(bytes.NewReader(buf)) + pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf) // if it's not a pointer, just serve the data directly if !pointer.IsValid() { - // First handle caching for the blob - if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { - return - } - - // If not cached - serve! - common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) + _, _ = ctx.Resp.Write(lfsPointerBuf) return } @@ -188,12 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // If there isn't one, just serve the data directly if errors.Is(err, git_model.ErrLFSObjectNotExist) { - // Handle caching for the blob SHA (not the LFS object OID) - if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { - return - } - - common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) + _, _ = ctx.Resp.Write(lfsPointerBuf) return } else if err != nil { ctx.APIErrorInternal(err) @@ -214,14 +192,13 @@ func GetRawFileOrLFS(ctx *context.APIContext) { } } - lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer) + lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { ctx.APIErrorInternal(err) return } - defer lfsDataRc.Close() - - common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc) + defer lfsDataFile.Close() + httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath}) } func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) { diff --git a/routers/common/actions.go b/routers/common/actions.go index 39d2111f5a..f698ba9436 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -10,6 +10,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -60,9 +61,8 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository ctx.ServeContent(reader, &context.ServeHeaderOptions{ Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), ContentLength: &task.LogSize, - ContentType: "text/plain", - ContentTypeCharset: "utf-8", - Disposition: "attachment", + ContentType: "text/plain; charset=utf-8", + ContentDisposition: httplib.ContentDispositionAttachment, }) return nil } diff --git a/routers/common/serve.go b/routers/common/serve.go index 4bb1a48b0d..9232d90c94 100644 --- a/routers/common/serve.go +++ b/routers/common/serve.go @@ -4,7 +4,6 @@ package common import ( - "io" "path" "time" @@ -12,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" @@ -24,29 +22,24 @@ func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, return nil } + if err := repo.LoadOwner(ctx); err != nil { + return err + } + dataRc, err := blob.DataAsync() if err != nil { return err } - defer func() { - if err = dataRc.Close(); err != nil { - log.Error("ServeBlob: Close: %v", err) - } - }() + defer dataRc.Close() - _ = repo.LoadOwner(ctx) - httplib.ServeContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, &httplib.ServeHeaderOptions{ + if lastModified == nil { + lastModified = new(time.Time) + } + httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, httplib.ServeHeaderOptions{ Filename: path.Base(filePath), - CacheIsPublic: !repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility == structs.VisibleTypePublic, + CacheIsPublic: !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic, CacheDuration: setting.StaticCacheTime, + LastModified: *lastModified, }) return nil } - -func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) { - httplib.ServeContentByReader(ctx.Req, ctx.Resp, size, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) -} - -func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) { - httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, modTime, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) -} diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go index 5395529d66..205ab2f8ea 100644 --- a/routers/web/admin/diagnosis.go +++ b/routers/web/admin/diagnosis.go @@ -18,10 +18,10 @@ import ( func MonitorDiagnosis(ctx *context.Context) { seconds := min(max(ctx.FormInt64("seconds"), 1), 300) - httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{ - ContentType: "application/zip", - Disposition: "attachment", - Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")), + httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{ + ContentType: "application/zip", + Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")), + ContentDisposition: httplib.ContentDispositionAttachment, }) zipWriter := zip.NewWriter(ctx.Resp) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 98d86f0bb3..90810a6d25 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/templates" @@ -716,8 +717,9 @@ func ArtifactsDownloadView(ctx *context_module.Context) { } } - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName)) - + // A v4 Artifact may only contain a single file + // Multiple files are uploaded as a single file archive + // All other cases fall back to the legacy v1–v3 zip handling below if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) { err := actions.DownloadArtifactV4(ctx.Base, artifacts[0]) if err != nil { @@ -729,34 +731,41 @@ func ArtifactsDownloadView(ctx *context_module.Context) { // Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend // Those need to be zipped for download - writer := zip.NewWriter(ctx.Resp) - defer writer.Close() - for _, art := range artifacts { + ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip")) + zipWriter := zip.NewWriter(ctx.Resp) + defer zipWriter.Close() + + writeArtifactToZip := func(art *actions_model.ActionArtifact) error { f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { - ctx.ServerError("ActionsArtifacts.Open", err) - return + return fmt.Errorf("ActionsArtifacts.Open: %w", err) } + defer f.Close() - var r io.ReadCloser - if art.ContentEncoding == "gzip" { + var r io.ReadCloser = f + if art.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip { r, err = gzip.NewReader(f) if err != nil { - ctx.ServerError("gzip.NewReader", err) - return + return fmt.Errorf("gzip.NewReader: %w", err) } - } else { - r = f } defer r.Close() - w, err := writer.Create(art.ArtifactPath) + w, err := zipWriter.Create(art.ArtifactPath) if err != nil { - ctx.ServerError("writer.Create", err) - return + return fmt.Errorf("zipWriter.Create: %w", err) } - if _, err := io.Copy(w, r); err != nil { - ctx.ServerError("io.Copy", err) + _, err = io.Copy(w, r) + if err != nil { + return fmt.Errorf("io.Copy: %w", err) + } + return nil + } + + for _, art := range artifacts { + err := writeArtifactToZip(art) + if err != nil { + ctx.ServerError("writeArtifactToZip", err) return } } diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 19d533f362..9b2c64049b 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -11,10 +11,10 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" @@ -199,7 +199,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { } defer fr.Close() - common.ServeContentByReadSeeker(ctx.Base, attach.Name, new(attach.CreatedUnix.AsTime()), fr) + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, fr, httplib.ServeHeaderOptions{Filename: attach.Name}) } // GetAttachment serve attachments diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 073d3d7420..25166ea1d3 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -10,8 +10,8 @@ import ( git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/routers/common" @@ -24,28 +24,15 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim return nil } - dataRc, err := blob.DataAsync() + lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) if err != nil { return err } - closed := false - defer func() { - if closed { - return - } - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - }() - pointer, _ := lfs.ReadPointer(dataRc) + pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf) if pointer.IsValid() { meta, _ := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid) if meta == nil { - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - closed = true return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified) } if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) { @@ -61,22 +48,14 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim } } - lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer) + lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { return err } - defer func() { - if err = lfsDataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - }() - common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc) + defer lfsDataFile.Close() + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath}) return nil } - if err = dataRc.Close(); err != nil { - log.Error("ServeBlobOrLFS: Close: %v", err) - } - closed = true return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified) } diff --git a/services/context/base.go b/services/context/base.go index 4baea95ccf..06ccefa3aa 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -173,12 +173,12 @@ func (b *Base) Redirect(location string, status ...int) { type ServeHeaderOptions httplib.ServeHeaderOptions func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt)) + httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opt)) } // ServeContent serves content to http request func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts)) + httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opts)) http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } diff --git a/services/lfs/server.go b/services/lfs/server.go index fc09eb58ca..d0fd841041 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -172,7 +172,7 @@ func DownloadHandler(ctx *context.Context) { if len(filename) > 0 { decodedFilename, err := base64.RawURLEncoding.DecodeString(filename) if err == nil { - ctx.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+string(decodedFilename)+"\"") + ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(string(decodedFilename))) ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") } } diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 1d28e00655..2431ae4b93 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -328,7 +328,7 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) error if setting.Repository.StreamArchives || len(archiveReq.Paths) > 0 { // the header must be set before starting streaming even an error would occur, // because errors may happen in git command and such cases aren't in our control. - httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{Filename: downloadName}) + httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{Filename: downloadName}) if err := archiveReq.Stream(ctx, ctx.Resp); err != nil && !ctx.Written() { if gitcmd.StderrHasPrefix(err, "fatal: pathspec") { return util.NewInvalidArgumentErrorf("path doesn't exist or is invalid") diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index 4127ae91f5..c0cd4cdebd 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -11,23 +11,28 @@ import ( "encoding/xml" "fmt" "io" + "mime" "net/http" "strings" "testing" "time" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/actions" actions_service "code.gitea.io/gitea/services/actions" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/timestamppb" @@ -48,15 +53,18 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { assert.NoError(t, err) table := []struct { - name string - version int32 - blockID bool - noLength bool - append int + name string + version int32 + contentType string + blockID bool + noLength bool + append int + path string }{ { name: "artifact", version: 4, + path: "artifact.zip", }, { name: "artifact2", @@ -98,6 +106,23 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { append: 4, blockID: true, }, + { + name: "artifact9.json", + version: 7, + contentType: "application/json", + }, + { + name: "artifact10", + version: 7, + contentType: "application/zip", + path: "artifact10.zip", + }, + { + name: "artifact11.zip", + version: 7, + contentType: "application/zip", + path: "artifact11.zip", + }, } for _, entry := range table { @@ -108,6 +133,7 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { Name: entry.name, WorkflowRunBackendId: "792", WorkflowJobRunBackendId: "193", + MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil), })).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) var uploadResp actions.CreateArtifactResponse @@ -120,9 +146,8 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { blocks := make([]string, 0, util.Iif(entry.blockID, entry.append+1, 0)) // get upload url - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") for i := range entry.append + 1 { - url := uploadResp.SignedUploadUrl[idx:] + url := uploadResp.SignedUploadUrl // See https://learn.microsoft.com/en-us/rest/api/storageservices/append-block // See https://learn.microsoft.com/en-us/rest/api/storageservices/put-block if entry.blockID { @@ -146,7 +171,7 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { if entry.blockID && entry.append > 0 { // https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list - blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" // upload artifact blockList blockList := &actions.BlockList{ Latest: blocks, @@ -174,6 +199,19 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) { var finalizeResp actions.FinalizeArtifactResponse protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) assert.True(t, finalizeResp.Ok) + + artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId}) + if entry.contentType != "" { + assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType) + } else { + assert.Equal(t, "application/zip", artifact.ContentEncodingOrType) + } + if entry.path != "" { + assert.Equal(t, entry.path, artifact.ArtifactPath) + } + assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status) + assert.Equal(t, int64(entry.append+1)*1024, artifact.FileSize) + assert.Equal(t, int64(entry.append+1)*1024, artifact.FileCompressedSize) }) } } @@ -198,8 +236,7 @@ func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) { assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload url - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + url := uploadResp.SignedUploadUrl + "&comp=block" // upload artifact chunk body := strings.Repeat("B", 1024) @@ -243,8 +280,7 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload url - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + url := uploadResp.SignedUploadUrl + "&comp=block" // upload artifact chunk body := strings.Repeat("A", 1024) @@ -290,9 +326,8 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") // get upload urls - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile" - blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + url := uploadResp.SignedUploadUrl + "&comp=block&blockid=%2f..%2fmyfile" + blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" // upload artifact chunk body := strings.Repeat("A", 1024) @@ -339,63 +374,126 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) - // acquire artifact upload url - req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ - Version: 4, - Name: "artifactWithChunksOutOfOrder", - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var uploadResp actions.CreateArtifactResponse - protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) - assert.True(t, uploadResp.Ok) - assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") - - // get upload urls - idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") - block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1" - block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2" - blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" - - // upload artifact chunks - bodyb := strings.Repeat("B", 1024) - req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb)) - MakeRequest(t, req, http.StatusCreated) - - bodya := strings.Repeat("A", 1024) - req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya)) - MakeRequest(t, req, http.StatusCreated) - - // upload artifact blockList - blockList := &actions.BlockList{ - Latest: []string{ - "block1", - "block2", - }, + table := []struct { + name string + artifactName string + serveDirect bool + contentType string + }{ + {name: "Upload-Zip", artifactName: "artifact-v4-upload", contentType: ""}, + {name: "Upload-Pdf", artifactName: "report-upload.pdf", contentType: "application/pdf"}, + {name: "Upload-Html", artifactName: "report-upload.html", contentType: "application/html"}, + {name: "ServeDirect-Zip", artifactName: "artifact-v4-upload-serve-direct", contentType: "", serveDirect: true}, + {name: "ServeDirect-Pdf", artifactName: "report-upload-serve-direct.pdf", contentType: "application/pdf", serveDirect: true}, + {name: "ServeDirect-Html", artifactName: "report-upload-serve-direct.html", contentType: "application/html", serveDirect: true}, } - rawBlockList, err := xml.Marshal(blockList) - assert.NoError(t, err) - req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) - MakeRequest(t, req, http.StatusCreated) - t.Logf("Create artifact confirm") + for _, entry := range table { + t.Run(entry.name, func(t *testing.T) { + // Only AzureBlobStorageType supports ServeDirect Uploads + switch setting.Actions.ArtifactStorage.Type { + case setting.AzureBlobStorageType: + defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.serveDirect)() + default: + if entry.serveDirect { + t.Skip() + } + } + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: util.Iif[int32](entry.contentType != "", 7, 4), + Name: entry.artifactName, + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil), + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + if !entry.serveDirect { + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + } - sha := sha256.Sum256([]byte(bodya + bodyb)) + // get upload urls + block1URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block1")) + block2URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block2")) + blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist" - // confirm artifact upload - req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ - Name: "artifactWithChunksOutOfOrder", - Size: 2048, - Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - var finalizeResp actions.FinalizeArtifactResponse - protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) - assert.True(t, finalizeResp.Ok) + // upload artifact chunks + bodyb := strings.Repeat("B", 1024) + req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb)) + if entry.serveDirect { + req.Request.RequestURI = "" + nresp, err := http.DefaultClient.Do(req.Request) + require.NoError(t, err) + nresp.Body.Close() + require.Equal(t, http.StatusCreated, nresp.StatusCode) + } else { + MakeRequest(t, req, http.StatusCreated) + } + + bodya := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya)) + if entry.serveDirect { + req.Request.RequestURI = "" + nresp, err := http.DefaultClient.Do(req.Request) + require.NoError(t, err) + nresp.Body.Close() + require.Equal(t, http.StatusCreated, nresp.StatusCode) + } else { + MakeRequest(t, req, http.StatusCreated) + } + + // upload artifact blockList + blockList := &actions.BlockList{ + Latest: []string{ + base64.RawURLEncoding.EncodeToString([]byte("block1")), + base64.RawURLEncoding.EncodeToString([]byte("block2")), + }, + } + rawBlockList, err := xml.Marshal(blockList) + assert.NoError(t, err) + req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) + if entry.serveDirect { + req.Request.RequestURI = "" + nresp, err := http.DefaultClient.Do(req.Request) + require.NoError(t, err) + nresp.Body.Close() + require.Equal(t, http.StatusCreated, nresp.StatusCode) + } else { + MakeRequest(t, req, http.StatusCreated) + } + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(bodya + bodyb)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: entry.artifactName, + Size: 2048, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) + + artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId}) + if entry.contentType != "" { + assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType) + } else { + assert.Equal(t, "application/zip", artifact.ContentEncodingOrType) + } + assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status) + assert.Equal(t, int64(2048), artifact.FileSize) + assert.Equal(t, int64(2048), artifact.FileCompressedSize) + }) + } } func TestActionsArtifactV4DownloadSingle(t *testing.T) { @@ -404,33 +502,97 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) { token, err := actions_service.CreateAuthorizationToken(48, 792, 193) assert.NoError(t, err) - // list artifacts by name - req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ - NameFilter: wrapperspb.String("artifact-v4-download"), - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })).AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - var listResp actions.ListArtifactsResponse - protojson.Unmarshal(resp.Body.Bytes(), &listResp) - assert.Len(t, listResp.Artifacts, 1) + table := []struct { + Name string + ArtifactName string + FileName string + ServeDirect bool + ContentType string + ContentDisposition string + }{ + {Name: "Download-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip"}, + {Name: "Download-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf"}, + {Name: "Download-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html"}, + {Name: "ServeDirect-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip", ServeDirect: true}, + {Name: "ServeDirect-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf", ServeDirect: true}, + {Name: "ServeDirect-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html", ServeDirect: true}, + } - // acquire artifact download url - req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ - Name: "artifact-v4-download", - WorkflowRunBackendId: "792", - WorkflowJobRunBackendId: "193", - })). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - var finalizeResp actions.GetSignedArtifactURLResponse - protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) - assert.NotEmpty(t, finalizeResp.SignedUrl) + for _, entry := range table { + t.Run(entry.Name, func(t *testing.T) { + switch setting.Actions.ArtifactStorage.Type { + case setting.AzureBlobStorageType: + defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.ServeDirect)() + case setting.MinioStorageType: + defer test.MockVariableValue(&setting.Actions.ArtifactStorage.MinioConfig.ServeDirect, entry.ServeDirect)() + default: + if entry.ServeDirect { + t.Skip() + } + } - req = NewRequest(t, "GET", finalizeResp.SignedUrl) - resp = MakeRequest(t, req, http.StatusOK) - body := strings.Repeat("D", 1024) - assert.Equal(t, body, resp.Body.String()) + // list artifacts by name + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ + NameFilter: wrapperspb.String(entry.ArtifactName), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var listResp actions.ListArtifactsResponse + require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp)) + require.Len(t, listResp.Artifacts, 1) + + // list artifacts by id + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ + IdFilter: wrapperspb.Int64(listResp.Artifacts[0].DatabaseId), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp)) + assert.Len(t, listResp.Artifacts, 1) + + // acquire artifact download url + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ + Name: entry.ArtifactName, + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.GetSignedArtifactURLResponse + require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)) + assert.NotEmpty(t, finalizeResp.SignedUrl) + + body := strings.Repeat("D", 1024) + var contentDisposition string + if entry.ServeDirect { + externalReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, finalizeResp.SignedUrl, nil) + require.NoError(t, err) + externalResp, err := http.DefaultClient.Do(externalReq) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, externalResp.StatusCode) + assert.Equal(t, entry.ContentType, externalResp.Header.Get("Content-Type")) + contentDisposition = externalResp.Header.Get("Content-Disposition") + buf := make([]byte, 1024) + n, err := io.ReadAtLeast(externalResp.Body, buf, len(buf)) + externalResp.Body.Close() + require.NoError(t, err) + assert.Equal(t, len(buf), n) + assert.Equal(t, body, string(buf)) + } else { + req = NewRequest(t, "GET", finalizeResp.SignedUrl) + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, entry.ContentType, resp.Header().Get("Content-Type")) + contentDisposition = resp.Header().Get("Content-Disposition") + assert.Equal(t, body, resp.Body.String()) + } + disposition, param, err := mime.ParseMediaType(contentDisposition) + require.NoError(t, err) + assert.Equal(t, "inline", disposition) + assert.Equal(t, entry.FileName, param["filename"]) + }) + } } func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) { @@ -561,7 +723,7 @@ func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) { for _, artifact := range listResp.Entries { assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID)) assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID)) - req = NewRequestWithBody(t, "GET", listResp.Entries[0].URL, nil). + req = NewRequestWithBody(t, "GET", artifact.URL, nil). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) From a3cc34472b4fe730aa8766d874ebf00c75cef2c8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:07:59 -0700 Subject: [PATCH 119/207] Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass `ServeHeaderOptions` by value instead of pointer across all call sites — no nil-check semantics are needed and the struct is small enough that copying is fine. ## Changes - **`services/context/base.go`**: `SetServeHeaders` and `ServeContent` accept `ServeHeaderOptions` (value, not pointer); internal unsafe pointer cast replaced with a clean type conversion - **`routers/api/packages/helper/helper.go`**: `ServePackageFile` variadic changed from `...*context.ServeHeaderOptions` to `...context.ServeHeaderOptions`; internal variable is now a value type - **All call sites** (13 files): `&context.ServeHeaderOptions{...}` → `context.ServeHeaderOptions{...}` Before/after at the definition level: ```go // Before func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { ... } func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { ... } func ServePackageFile(..., forceOpts ...*context.ServeHeaderOptions) { ... } // After func (b *Base) SetServeHeaders(opts ServeHeaderOptions) { ... } func (b *Base) ServeContent(r io.ReadSeeker, opts ServeHeaderOptions) { ... } func ServePackageFile(..., forceOpts ...context.ServeHeaderOptions) { ... } ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <2114189+wxiaoguang@users.noreply.github.com> Co-authored-by: wxiaoguang --- modules/httplib/serve.go | 2 +- routers/api/actions/artifacts.go | 2 +- routers/api/packages/alpine/alpine.go | 2 +- routers/api/packages/arch/arch.go | 4 +- routers/api/packages/debian/debian.go | 4 +- routers/api/packages/helper/helper.go | 6 +- routers/api/packages/maven/maven.go | 2 +- routers/api/packages/rpm/rpm.go | 4 +- routers/api/packages/rubygems/rubygems.go | 4 +- routers/api/packages/swift/swift.go | 4 +- routers/common/actions.go | 2 +- routers/web/user/setting/packages.go | 2 +- services/context/base.go | 10 +- services/repository/archiver/archiver.go | 2 +- tests/integration/download_test.go | 124 +++++++++------------- 15 files changed, 77 insertions(+), 97 deletions(-) diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index e8299d1c80..8abf6f1887 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -87,7 +87,7 @@ func serveSetHeadersByUserContent(w http.ResponseWriter, contentPrefetchBuf []by if setting.MimeTypeMap.Enabled { fileExtension := strings.ToLower(path.Ext(opts.Filename)) opts.ContentType = setting.MimeTypeMap.Map[fileExtension] - detectCharset = !strings.Contains(opts.ContentType, "charset=") + detectCharset = strings.HasPrefix(opts.ContentType, "text/") && !strings.Contains(opts.ContentType, "charset=") } if opts.ContentType == "" { diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index a6722616cf..13cbecb5cd 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -496,7 +496,7 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { ctx.Resp.Header().Set("Content-Encoding", "gzip") } log.Debug("[artifact] downloadArtifact, name: %s, path: %s, storage: %s, size: %d", artifact.ArtifactName, artifact.ArtifactPath, artifact.StoragePath, artifact.FileSize) - ctx.ServeContent(fd, &context.ServeHeaderOptions{ + ctx.ServeContent(fd, context.ServeHeaderOptions{ Filename: artifact.ArtifactName, LastModified: artifact.CreatedUnix.AsLocalTime(), }) diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index f250a1a549..52fc287a0e 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -54,7 +54,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/x-pem-file", Filename: fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)), }) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 5a124f6918..f3b70f39b6 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", }) } @@ -232,7 +232,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) { return } - ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{ + ctx.ServeContent(bytes.NewReader(data), context.ServeHeaderOptions{ Filename: filenameOrig, }) return diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index 82c7952bdb..785efb6dda 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", Filename: "repository.key", }) @@ -233,7 +233,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{ + helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{ ContentType: "application/vnd.debian.binary-package", Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go index 27d4e6ffdc..01ae5d2b7e 100644 --- a/routers/api/packages/helper/helper.go +++ b/routers/api/packages/helper/helper.go @@ -39,7 +39,7 @@ func ProcessErrorForUser(ctx *context.Context, status int, errObj any) string { // ServePackageFile the content of the package file // If the url is set it will redirect the request, otherwise the content is copied to the response. -func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) { +func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...context.ServeHeaderOptions) { if u != nil { ctx.Redirect(u.String()) return @@ -47,11 +47,11 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf defer s.Close() - var opts *context.ServeHeaderOptions + var opts context.ServeHeaderOptions if len(forceOpts) > 0 { opts = forceOpts[0] } else { - opts = &context.ServeHeaderOptions{ + opts = context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 6c2916908b..446398caf7 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -200,7 +200,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool return } - opts := &context.ServeHeaderOptions{ + opts := context.ServeHeaderOptions{ ContentLength: &pb.Size, LastModified: pf.CreatedUnix.AsLocalTime(), } diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index 5abbb0c8ae..4447a0c3cf 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -57,7 +57,7 @@ func GetRepositoryKey(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{ ContentType: "application/pgp-keys", Filename: "repository.key", }) @@ -80,7 +80,7 @@ func CheckRepositoryFileExistence(ctx *context.Context) { return } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: pf.Name, LastModified: pf.CreatedUnix.AsLocalTime(), }) diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 69764c1df3..fe2e7af6d9 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -79,7 +79,7 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo }) } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: filename + ".gz", }) @@ -119,7 +119,7 @@ func ServePackageSpecification(ctx *context.Context) { return } - ctx.SetServeHeaders(&context.ServeHeaderOptions{ + ctx.SetServeHeaders(context.ServeHeaderOptions{ Filename: filename, }) diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 66c28c9772..948ece7a27 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -281,7 +281,7 @@ func DownloadManifest(ctx *context.Context) { filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion) } - ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(m.Content), context.ServeHeaderOptions{ ContentType: "text/x-swift", Filename: filename, LastModified: pv.CreatedUnix.AsLocalTime(), @@ -437,7 +437,7 @@ func DownloadPackageFile(ctx *context.Context) { Digest: pd.Files[0].Blob.HashSHA256, }) - helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{ + helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{ Filename: pf.Name, ContentType: "application/zip", LastModified: pf.CreatedUnix.AsLocalTime(), diff --git a/routers/common/actions.go b/routers/common/actions.go index f698ba9436..4eb7078db6 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -58,7 +58,7 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository if p := strings.Index(workflowName, "."); p > 0 { workflowName = workflowName[0:p] } - ctx.ServeContent(reader, &context.ServeHeaderOptions{ + ctx.ServeContent(reader, context.ServeHeaderOptions{ Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), ContentLength: &task.LogSize, ContentType: "text/plain; charset=utf-8", diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go index 62b0240642..66aa241377 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -112,7 +112,7 @@ func RegenerateChefKeyPair(ctx *context.Context) { return } - ctx.ServeContent(strings.NewReader(priv), &context.ServeHeaderOptions{ + ctx.ServeContent(strings.NewReader(priv), context.ServeHeaderOptions{ ContentType: "application/x-pem-file", Filename: ctx.Doer.Name + ".priv", }) diff --git a/services/context/base.go b/services/context/base.go index 06ccefa3aa..8d44de5bc7 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -170,15 +170,15 @@ func (b *Base) Redirect(location string, status ...int) { http.Redirect(b.Resp, b.Req, location, code) } -type ServeHeaderOptions httplib.ServeHeaderOptions +type ServeHeaderOptions = httplib.ServeHeaderOptions -func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opt)) +func (b *Base) SetServeHeaders(opts ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, opts) } // ServeContent serves content to http request -func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { - httplib.ServeSetHeaders(b.Resp, *(*httplib.ServeHeaderOptions)(opts)) +func (b *Base) ServeContent(r io.ReadSeeker, opts ServeHeaderOptions) { + httplib.ServeSetHeaders(b.Resp, opts) http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 2431ae4b93..f7069f226b 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -359,7 +359,7 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) error } defer fr.Close() - ctx.ServeContent(fr, &gitea_context.ServeHeaderOptions{ + ctx.ServeContent(fr, gitea_context.ServeHeaderOptions{ Filename: downloadName, LastModified: archiver.CreatedUnix.AsLocalTime(), }) diff --git a/tests/integration/download_test.go b/tests/integration/download_test.go index efe5ac791c..3e7be98b09 100644 --- a/tests/integration/download_test.go +++ b/tests/integration/download_test.go @@ -8,86 +8,66 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -func TestDownloadByID(t *testing.T) { +func TestDownloadRepoContent(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") - resp := session.MakeRequest(t, req, http.StatusOK) + t.Run("RawBlob", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) + }) - assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) -} - -func TestDownloadByIDForSVGUsesSecureHeaders(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) - assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) - assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) -} - -func TestDownloadByIDMedia(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) -} - -func TestDownloadByIDMediaForSVGUsesSecureHeaders(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - // Request raw blob - req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) - assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) - assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) -} - -func TestDownloadRawTextFileWithoutMimeTypeMapping(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user2") - - req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) -} - -func TestDownloadRawTextFileWithMimeTypeMapping(t *testing.T) { - defer tests.PrepareTestEnv(t)() - setting.MimeTypeMap.Map[".xml"] = "text/xml" - setting.MimeTypeMap.Enabled = true - - session := loginUser(t, "user2") - - req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") - resp := session.MakeRequest(t, req, http.StatusOK) - - assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type")) - - delete(setting.MimeTypeMap.Map, ".xml") - setting.MimeTypeMap.Enabled = false + t.Run("SVGUsesSecureHeaders", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) + assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) + assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) + }) + + t.Run("MediaBlob", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String()) + }) + + t.Run("MediaSVGUsesSecureHeaders", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy")) + assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) + assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options")) + }) + + t.Run("MimeTypeMap", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp := session.MakeRequest(t, req, http.StatusOK) + // although the file is a valid XML file, it is served as "text/plain" to avoid site content spamming (the same to "text/html" files) + assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.MimeTypeMap)() + setting.MimeTypeMap.Enabled = true + + setting.MimeTypeMap.Map[".xml"] = "text/xml" + req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp = session.MakeRequest(t, req, http.StatusOK) + // respect the mime mapping, and "text/plain" protection isn't used anymore + assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Equal(t, "inline; filename=test.xml", resp.Header().Get("Content-Disposition")) + + setting.MimeTypeMap.Map[".xml"] = "application/xml" + req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml") + resp = session.MakeRequest(t, req, http.StatusOK) + // non-text file don't have "charset" + assert.Equal(t, "application/xml", resp.Header().Get("Content-Type")) + }) } From ffa626b585225d62718f39e1b5fcc00416b0b7e4 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 26 Mar 2026 00:53:31 +0000 Subject: [PATCH 120/207] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 663de75772..894502ce10 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -969,7 +969,6 @@ "repo.visibility_description": "Ní bheidh ach an t-úinéir nó baill na heagraíochta má tá cearta acu in ann é a fheiceáil.", "repo.visibility_helper": "Déan stóras príobháideach", "repo.visibility_helper_forced": "Cuireann riarthóir do shuíomh iallach ar stórais nua a bheith príobháideach.", - "repo.visibility_fork_helper": "(Beidh tionchar ag athrú seo ar gach forc.)", "repo.clone_helper": "Teastaíonn cabhair ó chlónáil? Tabhair cuairt ar Cabhair.", "repo.fork_repo": "Stóras Forc", "repo.fork_from": "Forc ó", @@ -2174,7 +2173,8 @@ "repo.settings.transfer_abort_invalid": "Ní féidir leat aistriú stóras nach bhfuil ann a chealú.", "repo.settings.transfer_abort_success": "Cuireadh an t-aistriú stóras chuig %s ar ceal go rathúil.", "repo.settings.transfer_desc": "Aistrigh an stóras seo chuig úsáideoir nó chuig eagraíocht a bhfuil cearta riarthóra agat ina leith.", - "repo.settings.transfer_form_title": "Cuir isteach ainm an stóras mar dhearbhú:", + "repo.settings.enter_repo_name_to_confirm": "Cuir isteach ainm an stórais mar dheimhniú:", + "repo.settings.enter_repo_full_name_to_confirm": "Cuir isteach ainm iomlán an stórais (úinéir/ainm) mar dheimhniú:", "repo.settings.transfer_in_progress": "Tá aistriú ar siúl faoi láthair. Cealaigh é más mian leat an stóras seo a aistriú chuig úsáideoir eile.", "repo.settings.transfer_notices_1": "- Caillfidh tú rochtain ar an stóras má aistríonn tú é chuig úsáideoir aonair.", "repo.settings.transfer_notices_2": "- Coimeádfaidh tú rochtain ar an stóras má aistríonn tú é chuig eagraíocht a bhfuil (comh)úinéir agat.", @@ -2474,10 +2474,13 @@ "repo.settings.matrix.room_id": "ID seomra", "repo.settings.matrix.message_type": "Cineál teachtaireachta", "repo.settings.visibility.private.button": "Déan Príobháideach", - "repo.settings.visibility.private.text": "Má athraítear an infheictheacht go príobháideach, ní bheidh an stór le feiceáil ach ag baill cheadaithe agus d’fhéadfadh sé go mbainfí an gaol idir é agus forcanna, faireoirí agus réaltaí atá ann cheana féin.", + "repo.settings.visibility.private.text": "Má athraítear an infheictheacht go príobháideach, ní bheidh an stór le feiceáil ach ag baill cheadaithe agus d’fhéadfadh sé go mbainfí an gaol idir é agus forcanna, breathnóirí agus réaltaí atá ann cheana féin.", "repo.settings.visibility.private.bullet_title": "An infheictheacht a athrú go toil phríobháide", "repo.settings.visibility.private.bullet_one": "Déan an stóras le feiceáil ag baill cheadaithe amháin.", - "repo.settings.visibility.private.bullet_two": "D’fhéadfadh sé an gaol idir é agus forcanna, faireoirí, agus réaltaí a bhaint.", + "repo.settings.visibility.private.bullet_two": "Cuir an infheictheacht i bhfeidhm ar a fhorcanna, agus bain na breathnóirí agus na réaltaí.", + "repo.settings.visibility.private.stats_stars": "Tá %d réalta(í) sa stórlann seo a d'fhéadfadh a bheith caillte.", + "repo.settings.visibility.private.stats_watchers": "Tá %d breathnóir(í) sa stórlann seo a d'fhéadfadh a bheith caillte.", + "repo.settings.visibility.private.stats_forks": "Tá %d forc(anna) bainteach leis an stórlann seo.", "repo.settings.visibility.public.button": "Déan Poiblí", "repo.settings.visibility.public.text": "Má athraíonn an infheictheacht don phobal, beidh an stóras le feiceáil do dhuine ar bith.", "repo.settings.visibility.public.bullet_title": "Athróidh an infheictheacht go poiblí:", From 9583e1a65c5f11c4aa66e2e8656cde9e70d9b5a8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Mar 2026 10:48:09 +0100 Subject: [PATCH 121/207] Linkify URLs in Actions workflow logs (#36986) Detect URLs in Actions log output and render them as clickable links, similar to how GitHub Actions handles this. Pre-existing links from ansi_up's OSC 8 parsing are also kept intact. --------- Signed-off-by: silverwind Co-authored-by: Claude (claude-opus-4-6) Co-authored-by: wxiaoguang --- web_src/css/themes/theme-gitea-dark.css | 1 + web_src/css/themes/theme-gitea-light.css | 1 + web_src/js/components/ActionRunJobView.vue | 5 ++++ web_src/js/render/ansi.test.ts | 5 ++++ web_src/js/render/ansi.ts | 29 ++++++++++++---------- web_src/js/utils/url.test.ts | 29 +++++++++++++++++++++- web_src/js/utils/url.ts | 28 +++++++++++++++++++++ 7 files changed, 84 insertions(+), 14 deletions(-) diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index f347589509..c62c20f93a 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -74,6 +74,7 @@ gitea-theme-meta-info { --color-console-active-bg: #2e353b; --color-console-menu-bg: #262b31; --color-console-menu-border: #414b55; + --color-console-link: #8f9ba8; /* named colors */ --color-red: #cc4848; --color-orange: #cc580c; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index dc916f002d..5f437c5a6c 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -74,6 +74,7 @@ gitea-theme-meta-info { --color-console-active-bg: #d0d7de; --color-console-menu-bg: #f8f9fb; --color-console-menu-border: #d0d7de; + --color-console-link: #5c656d; /* named colors */ --color-red: #db2828; --color-orange: #f2711c; diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 9d8ee0dbde..747889d04c 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -641,6 +641,11 @@ async function hashChangeListener() { overflow-wrap: anywhere; } +.job-step-logs .log-msg a { + color: var(--color-console-link) !important; + text-decoration: underline; +} + .job-step-logs .job-log-line .log-cmd-command { color: var(--color-ansi-blue); } diff --git a/web_src/js/render/ansi.test.ts b/web_src/js/render/ansi.test.ts index 21b7523994..2d9b8ede00 100644 --- a/web_src/js/render/ansi.test.ts +++ b/web_src/js/render/ansi.test.ts @@ -17,4 +17,9 @@ test('renderAnsi', () => { // treat "\033[0K" and "\033[0J" (Erase display/line) as "\r", then it will be covered to "\n" finally. expect(renderAnsi('a\x1b[Kb\x1b[2Jc')).toEqual('a\nb\nc'); expect(renderAnsi('\x1b[48;5;88ma\x1b[38;208;48;5;159mb\x1b[m')).toEqual(`ab`); + + // URLs in ANSI output become clickable links + const link = (url: string) => `${url}`; + expect(renderAnsi('Downloading https://github.com/actions/upload-artifact/releases')).toEqual(`Downloading ${link('https://github.com/actions/upload-artifact/releases')}`); + expect(renderAnsi('\x1b[32mhttps://proxy.golang.org/cached-only\x1b[0m')).toEqual(`${link('https://proxy.golang.org/cached-only')}`); }); diff --git a/web_src/js/render/ansi.ts b/web_src/js/render/ansi.ts index f5429ef6ad..4625e54233 100644 --- a/web_src/js/render/ansi.ts +++ b/web_src/js/render/ansi.ts @@ -1,4 +1,5 @@ import {AnsiUp} from 'ansi_up'; +import {linkifyURLs} from '../utils/url.ts'; const replacements: Array<[RegExp, string]> = [ [/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op @@ -25,21 +26,23 @@ export function renderAnsi(line: string): string { } } + let result: string; if (!line.includes('\r')) { - return ansi_up.ansi_to_html(line); - } - - // handle "\rReading...1%\rReading...5%\rReading...100%", - // convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%" - const lines: Array = []; - for (const part of line.split('\r')) { - if (part === '') continue; - const partHtml = ansi_up.ansi_to_html(part); - if (partHtml !== '') { - lines.push(partHtml); + result = ansi_up.ansi_to_html(line); + } else { + // handle "\rReading...1%\rReading...5%\rReading...100%", + // convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%" + const lines: Array = []; + for (const part of line.split('\r')) { + if (part === '') continue; + const partHtml = ansi_up.ansi_to_html(part); + if (partHtml !== '') { + lines.push(partHtml); + } } + // the log message element is with "white-space: break-spaces;", so use "\n" to break lines + result = lines.join('\n'); } - // the log message element is with "white-space: break-spaces;", so use "\n" to break lines - return lines.join('\n'); + return linkifyURLs(result); } diff --git a/web_src/js/utils/url.test.ts b/web_src/js/utils/url.test.ts index c39dd15732..3a4323e88f 100644 --- a/web_src/js/utils/url.test.ts +++ b/web_src/js/utils/url.test.ts @@ -1,10 +1,37 @@ -import {pathEscapeSegments, toOriginUrl} from './url.ts'; +import {linkifyURLs, pathEscapeSegments, toOriginUrl} from './url.ts'; test('pathEscapeSegments', () => { expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c'); expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c'); }); +test('linkifyURLs', () => { + const link = (url: string) => `${url}`; + expect(linkifyURLs('https://example.com')).toEqual(link('https://example.com')); + expect(linkifyURLs('https://dl.google.com/go/go1.23.6.linux-amd64.tar.gz')).toEqual(link('https://dl.google.com/go/go1.23.6.linux-amd64.tar.gz')); + expect(linkifyURLs('https://example.com/path?query=1&b=2#frag')).toEqual(link('https://example.com/path?query=1&b=2#frag')); + expect(linkifyURLs('visit https://example.com/repo for info')).toEqual(`visit ${link('https://example.com/repo')} for info`); + expect(linkifyURLs('See https://example.com.')).toEqual(`See ${link('https://example.com')}.`); + expect(linkifyURLs('https://example.com, and more')).toEqual(`${link('https://example.com')}, and more`); + expect(linkifyURLs('https://proxy.golang.org/cached-only')).toEqual(`${link('https://proxy.golang.org/cached-only')}`); + expect(linkifyURLs('https://registry.npmjs.org/@types/node')).toEqual(`${link('https://registry.npmjs.org/@types/node')}`); + expect(linkifyURLs('https://a.com and https://b.org')).toEqual(`${link('https://a.com')} and ${link('https://b.org')}`); + expect(linkifyURLs('no urls here')).toEqual('no urls here'); + expect(linkifyURLs('http://example.com/path')).toEqual(link('http://example.com/path')); + expect(linkifyURLs('http://localhost:3000/repo')).toEqual(link('http://localhost:3000/repo')); + expect(linkifyURLs('https://')).toEqual('https://'); + expect(linkifyURLs('Click here')).toEqual('Click here'); + expect(linkifyURLs('Click here')).toEqual('Click here'); + expect(linkifyURLs('https://example.com')).toEqual('https://example.com'); + expect(linkifyURLs('https://evil.com/')).toEqual(`${link('https://evil.com/')}`); + expect(linkifyURLs('https://evil.com/"onmouseover="alert(1)')).toEqual(`${link('https://evil.com/')}"onmouseover="alert(1)`); + expect(linkifyURLs('javascript:alert(1)')).toEqual('javascript:alert(1)'); // eslint-disable-line no-script-url + expect(linkifyURLs("https://evil.com/'onclick='alert(1)")).toEqual(`${link('https://evil.com/')}'onclick='alert(1)`); + expect(linkifyURLs('data:text/html,')).toEqual('data:text/html,'); + expect(linkifyURLs('https://evil.com/\nonclick=alert(1)')).toEqual(`${link('https://evil.com/')}\nonclick=alert(1)`); + expect(linkifyURLs('https://evil.com/"onmouseover=alert(1)')).toEqual(`${link('https://evil.com/"onmouseover=alert')}(1)`); +}); + test('toOriginUrl', () => { const oldLocation = String(window.location); for (const origin of ['https://example.com', 'https://example.com:3000']) { diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts index 6bcb4c1609..469693373a 100644 --- a/web_src/js/utils/url.ts +++ b/web_src/js/utils/url.ts @@ -2,6 +2,34 @@ export function pathEscapeSegments(s: string): string { return s.split('/').map(encodeURIComponent).join('/'); } +// Match HTML tags (to skip) or URLs (to linkify) in HTML content +const urlLinkifyPattern = /(<([-\w]+)[^>]*>)|(<\/([-\w]+)[^>]*>)|(https?:\/\/[^\s<>"'`|(){}[\]]+)/gi; +const trailingPunctPattern = /[.,;:!?]+$/; + +// Convert URLs to clickable links in HTML, preserving existing HTML tags +export function linkifyURLs(html: string): string { + let inAnchor = false; + return html.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => { + // skip URLs inside existing tags + if (openTag === 'a') { + inAnchor = true; + return match; + } else if (closeTag === 'a') { + inAnchor = false; + return match; + } + if (inAnchor || !url) { + return match; + } + + const trailingPunct = url.match(trailingPunctPattern); + const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url; + const trailing = trailingPunct ? trailingPunct[0] : ''; + // safe because regexp only matches valid URLs (no quotes or angle brackets) + return `${cleanUrl}${trailing}`; // eslint-disable-line github/unescaped-html-literal + }); +} + /** Convert an absolute or relative URL to an absolute URL with the current origin. It only * processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. */ export function toOriginUrl(urlStr: string) { From d5a89805d90d31465ac10fdf3d1a9119b669e8be Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Mar 2026 11:18:50 +0100 Subject: [PATCH 122/207] Improve severity labels in Actions logs and tweak colors (#36993) Add support for error, warning, notice, and debug log commands with bold label prefixes and colored backgrounds matching GitHub's style. Parse both `##[cmd]` and `::cmd args::` formats. Also improved the severity colors globally and added a devtest page for these. --------- Co-authored-by: Claude (claude-opus-4-6) --- templates/devtest/severity-colors.tmpl | 80 +++++++++++++++++++++ web_src/css/base.css | 6 -- web_src/css/modules/message.css | 23 +----- web_src/css/themes/theme-gitea-dark.css | 20 +++--- web_src/css/themes/theme-gitea-light.css | 28 ++++---- web_src/js/components/ActionRunJobView.vue | 27 ++++++- web_src/js/components/ActionRunView.test.ts | 10 ++- web_src/js/components/ActionRunView.ts | 32 ++++++++- 8 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 templates/devtest/severity-colors.tmpl diff --git a/templates/devtest/severity-colors.tmpl b/templates/devtest/severity-colors.tmpl new file mode 100644 index 0000000000..9f86b864ea --- /dev/null +++ b/templates/devtest/severity-colors.tmpl @@ -0,0 +1,80 @@ +{{template "devtest/devtest-header"}} +
    +

    Severity Colors

    + +

    Messages

    +
    +
    Error Message
    +

    This is an error message using --color-error-* variables.

    +
    +
    +
    Warning Message
    +

    This is a warning message using --color-warning-* variables.

    +
    +
    +
    Success Message
    +

    This is a success message using --color-success-* variables.

    +
    +
    +
    Info Message
    +

    This is an info message using --color-info-* variables.

    +
    + +

    Form Fields

    +
    +
    + + +
    +
    + +

    Labels

    +
    +
    Red
    +
    Orange
    +
    Yellow
    +
    Green
    +
    Blue
    +
    Violet
    +
    Purple
    +
    + +

    Color Swatches

    +

    Error

    +
    +
    +
    Text
    + error-bg +
    +
    +
    Hover
    + error-bg-hover +
    +
    +
    Active
    + error-bg-active +
    +
    +

    Warning

    +
    +
    +
    Text
    + warning-bg +
    +
    +

    Success

    +
    +
    +
    Text
    + success-bg +
    +
    +

    Info

    +
    +
    +
    Text
    + info-bg +
    +
    +
    +{{template "devtest/devtest-footer"}} diff --git a/web_src/css/base.css b/web_src/css/base.css index 2c7bd7395a..b4139c0e72 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -393,12 +393,6 @@ img.ui.avatar, aspect-ratio: 1; } -.ui.error.message .header, -.ui.warning.message .header { - color: inherit; - filter: saturate(2); -} - .full.height { flex-grow: 1; padding-bottom: var(--page-space-bottom); diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css index 7e8a2cf744..ce997c4350 100644 --- a/web_src/css/modules/message.css +++ b/web_src/css/modules/message.css @@ -41,9 +41,9 @@ margin-bottom: 1em; } -.ui.info.message .header, -.ui.blue.message .header { - color: var(--color-blue); +.ui.message .header { + color: inherit; + filter: saturate(2); } .ui.info.message, @@ -55,12 +55,6 @@ border-color: var(--color-info-border); } -.ui.success.message .header, -.ui.positive.message .header, -.ui.green.message .header { - color: var(--color-green); -} - .ui.success.message, .ui.attached.success.message, .ui.positive.message, @@ -70,12 +64,6 @@ border-color: var(--color-success-border); } -.ui.error.message .header, -.ui.negative.message .header, -.ui.red.message .header { - color: var(--color-red); -} - .ui.error.message, .ui.attached.error.message, .ui.red.message, @@ -87,11 +75,6 @@ border-color: var(--color-error-border); } -.ui.warning.message .header, -.ui.yellow.message .header { - color: var(--color-yellow); -} - .ui.warning.message, .ui.attached.warning.message, .ui.yellow.message, diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index c62c20f93a..fbdef1e2fb 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -162,20 +162,20 @@ gitea-theme-meta-info { --color-diff-removed-row-border: #634343; --color-diff-removed-word-bg: #6f3333; --color-diff-inactive: #22282d; - --color-error-border: #a04141; - --color-error-bg: #522; - --color-error-bg-active: #744; - --color-error-bg-hover: #633; - --color-error-text: #f9cbcb; + --color-error-border: #da3633; + --color-error-bg: #3c2425; + --color-error-bg-active: #5a3637; + --color-error-bg-hover: #4c2d2e; + --color-error-text: #f5817c; --color-success-border: #458a57; --color-success-bg: #284034; - --color-success-text: #6cc664; - --color-warning-border: #bb9d00; - --color-warning-bg: #3a3a30; - --color-warning-text: #fbbd08; + --color-success-text: #69be61; + --color-warning-border: #9e6a03; + --color-warning-bg: #2f2a1b; + --color-warning-text: #d29922; --color-info-border: #306090; --color-info-bg: #26354c; - --color-info-text: #38a8e8; + --color-info-text: #48b7f8; --color-red-badge: #db2828; --color-red-badge-bg: #db28281a; --color-red-badge-hover-bg: #db28284d; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 5f437c5a6c..761cb18da0 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -162,20 +162,20 @@ gitea-theme-meta-info { --color-diff-removed-row-border: #f1c0c0; --color-diff-removed-word-bg: #fdb8c0; --color-diff-inactive: #f0f2f4; - --color-error-border: #e0b4b4; - --color-error-bg: #fff6f6; - --color-error-bg-active: #fbb; - --color-error-bg-hover: #fdd; - --color-error-text: #9f3a38; - --color-success-border: #a3c293; - --color-success-bg: #fcfff5; - --color-success-text: #2c662d; - --color-warning-border: #c9ba9b; - --color-warning-bg: #fffaf3; - --color-warning-text: #573a08; - --color-info-border: #a9d5de; - --color-info-bg: #f8ffff; - --color-info-text: #276f86; + --color-error-border: #d63333; + --color-error-bg: #ffebeb; + --color-error-bg-active: #fdd; + --color-error-bg-hover: #fee; + --color-error-text: #8a3231; + --color-success-border: #49842b; + --color-success-bg: #eef6e4; + --color-success-text: #2f6e30; + --color-warning-border: #bf8700; + --color-warning-bg: #fff8e1; + --color-warning-text: #744500; + --color-info-border: #2d8fa8; + --color-info-bg: #e8f4fd; + --color-info-text: #216078; --color-red-badge: #db2828; --color-red-badge-bg: #db28281a; --color-red-badge-hover-bg: #db28284d; diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 747889d04c..fba78917c9 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -229,7 +229,8 @@ function createLogLine(stepIndex: number, startTime: number, line: LogLine, cmd: toggleElem(logTimeStamp, timeVisible.value['log-time-stamp']); toggleElem(logTimeSeconds, timeVisible.value['log-time-seconds']); - return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: 'job-log-line'}, + const lineClass = cmd?.name ? `job-log-line log-line-${cmd.name}` : 'job-log-line'; + return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: lineClass}, lineNum, logTimeStamp, logMsg, logTimeSeconds, ); } @@ -650,8 +651,28 @@ async function hashChangeListener() { color: var(--color-ansi-blue); } -.job-step-logs .job-log-line .log-cmd-error { - color: var(--color-ansi-red); +.job-step-logs .log-msg-label { + font-weight: var(--font-weight-semibold); +} + +.job-step-logs .log-line-error { + background: var(--color-error-bg); +} + +.job-step-logs .log-line-warning { + background: var(--color-warning-bg); +} + +.job-step-logs .log-cmd-error > .log-msg-label { + color: var(--color-error-text); +} + +.job-step-logs .log-cmd-warning > .log-msg-label { + color: var(--color-warning-text); +} + +.job-step-logs .log-cmd-debug { + color: var(--color-violet); } /* selectors here are intentionally exact to only match fullscreen */ diff --git a/web_src/js/components/ActionRunView.test.ts b/web_src/js/components/ActionRunView.test.ts index f0e3fa090a..1f972b73c0 100644 --- a/web_src/js/components/ActionRunView.test.ts +++ b/web_src/js/components/ActionRunView.test.ts @@ -8,8 +8,14 @@ test('LogLineMessage', () => { '##[endgroup]': '', '::endgroup::': '', - // parser shouldn't do any trim, keep origin output as-is - '##[error] foo': ' foo', + '##[error] foo': 'Error: foo', + '##[warning] foo': 'Warning: foo', + '##[notice] foo': 'Notice: foo', + '##[debug] foo': 'Debug: foo', + '::error::foo': 'Error: foo', + '::warning file=test.js,line=1::foo': 'Warning: foo', + '::notice::foo': 'Notice: foo', + '::debug::foo': 'Debug: foo', '[command] foo': ' foo', // hidden is special, it is actually skipped before creating diff --git a/web_src/js/components/ActionRunView.ts b/web_src/js/components/ActionRunView.ts index 6ae09a46fe..250f39e811 100644 --- a/web_src/js/components/ActionRunView.ts +++ b/web_src/js/components/ActionRunView.ts @@ -17,6 +17,9 @@ const LogLinePrefixCommandMap: Record = { '##[endgroup]': 'endgroup', '##[error]': 'error', + '##[warning]': 'warning', + '##[notice]': 'notice', + '##[debug]': 'debug', '[command]': 'command', // https://github.com/actions/toolkit/blob/master/docs/commands.md @@ -26,13 +29,16 @@ const LogLinePrefixCommandMap: Record = { '::remove-matcher': 'hidden', // it has arguments }; +// Pattern for ::cmd:: and ::cmd args:: format (args are stripped for display) +const LogLineCmdPattern = /^::(error|warning|notice|debug)(?:\s[^:]*)?::/; + export type LogLine = { index: number; timestamp: number; message: string; }; -export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'hidden'; +export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'warning' | 'notice' | 'debug' | 'hidden'; export type LogLineCommand = { name: LogLineCommandName, prefix: string, @@ -45,19 +51,39 @@ export function parseLogLineCommand(line: LogLine): LogLineCommand | null { return {name: LogLinePrefixCommandMap[prefix], prefix}; } } + // Handle ::cmd:: and ::cmd args:: format (runner may pass these through raw) + const match = LogLineCmdPattern.exec(line.message); + if (match) { + return {name: match[1] as LogLineCommandName, prefix: match[0]}; + } return null; } +const LogLineLabelMap: Partial> = { + 'error': 'Error', + 'warning': 'Warning', + 'notice': 'Notice', + 'debug': 'Debug', +}; + export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null) { const logMsgAttrs = {class: 'log-msg'}; - if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd?.name}`; // make it easier to add styles to some commands like "error" + if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd.name}`; // make it easier to add styles to some commands like "error" // TODO: for some commands (::group::), the "prefix removal" works well, for some commands with "arguments" (::remove-matcher ...::), // it needs to do further processing in the future (fortunately, at the moment we don't need to handle these commands) const msgContent = cmd ? line.message.substring(cmd.prefix.length) : line.message; const logMsg = createElementFromAttrs('span', logMsgAttrs); - logMsg.innerHTML = renderAnsi(msgContent); + const label = cmd ? LogLineLabelMap[cmd.name] : null; + if (label) { + logMsg.append(createElementFromAttrs('span', {class: 'log-msg-label'}, `${label}:`)); + const msgSpan = document.createElement('span'); + msgSpan.innerHTML = ` ${renderAnsi(msgContent.trimStart())}`; + logMsg.append(msgSpan); + } else { + logMsg.innerHTML = renderAnsi(msgContent); + } return logMsg; } From 8fdd6d1235393f6e5cd3027872121a3a9868d3d1 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Thu, 26 Mar 2026 12:48:04 -0600 Subject: [PATCH 123/207] Fix missing `workflow_run` notifications when updating jobs from multiple runs (#36997) This PR fixes `notifyWorkflowJobStatusUpdate` to send `WorkflowRunStatusUpdate` for each affected workflow run instead of only the first run in the input job list. --- services/actions/clear_tasks.go | 9 ++- tests/integration/repo_webhook_test.go | 83 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index e49bda1b16..c71f63e7d1 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -40,6 +40,8 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac if len(jobs) == 0 { return } + // The input jobs may belong to different runs, so track each affected run. + runs := make(map[int64]*actions_model.ActionRun, len(jobs)) for _, job := range jobs { if err := job.LoadAttributes(ctx); err != nil { log.Error("Failed to load job attributes: %v", err) @@ -47,10 +49,13 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac } CreateCommitStatusForRunJobs(ctx, job.Run, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + if _, ok := runs[job.RunID]; !ok { + runs[job.RunID] = job.Run + } } - if job := jobs[0]; job.Run != nil && job.Run.Repo != nil { - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + for _, run := range runs { + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) } } diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index a90f50078e..9ac9cced70 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/repo" @@ -1146,6 +1147,10 @@ func Test_WebhookWorkflowRun(t *testing.T) { testWorkflowRunEventsOnCancellingAbandonedRun(t, webhookData, false) }, }, + { + name: "WorkflowRunOnStoppingEndlessTasksForMultipleRuns", + testFunc: testWorkflowRunOnStoppingEndlessTasksForMultipleRuns, + }, } for _, obj := range testCases { t.Run(obj.name, func(t *testing.T) { @@ -1576,6 +1581,84 @@ jobs: assert.Equal(t, "user2/"+repoName, webhookData.payloads[1].Repo.FullName) } +func testWorkflowRunOnStoppingEndlessTasksForMultipleRuns(t *testing.T, webhookData *workflowRunWebhook) { + defer test.MockVariableValue(&setting.Actions.EndlessTaskTimeout, time.Second)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + repoName := "test-workflow-run-stop-endless-tasks" + testRepo := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: createActionsTestRepo(t, token, repoName, false).ID}) + + testAPICreateWebhookForRepo(t, session, "user2", repoName, webhookData.URL, "workflow_run") + + runners := make([]*mockRunner, 2) + for i := range runners { + runners[i] = newMockRunner() + runners[i].registerAsRepoRunner(t, "user2", repoName, fmt.Sprintf("mock-runner-%d", i), []string{"ubuntu-latest"}, false) + } + + workflowPath1 := ".gitea/workflows/endless-1.yml" + workflowPath2 := ".gitea/workflows/endless-2.yml" + workflowContent1 := `name: endless-1 +on: + push: + paths: + - '.gitea/workflows/endless-1.yml' +jobs: + job-1: + runs-on: ubuntu-latest + steps: + - run: echo 'job-1' +` + workflowContent2 := `name: endless-2 +on: + push: + paths: + - '.gitea/workflows/endless-2.yml' +jobs: + job-2: + runs-on: ubuntu-latest + steps: + - run: echo 'job-2' +` + + opts1 := getWorkflowCreateFileOptions(user2, testRepo.DefaultBranch, "create "+workflowPath1, workflowContent1) + createWorkflowFile(t, token, "user2", repoName, workflowPath1, opts1) + opts2 := getWorkflowCreateFileOptions(user2, testRepo.DefaultBranch, "create "+workflowPath2, workflowContent2) + createWorkflowFile(t, token, "user2", repoName, workflowPath2, opts2) + + task1 := runners[0].fetchTask(t) + task2 := runners[1].fetchTask(t) + _, job1, _ := getTaskAndJobAndRunByTaskID(t, task1.Id) + _, job2, _ := getTaskAndJobAndRunByTaskID(t, task2.Id) + require.NotEqual(t, job1.RunID, job2.RunID) + + initialRunEventsLen := len(webhookData.payloads) + + time.Sleep(2 * time.Second) + + require.NoError(t, actions.StopEndlessTasks(t.Context())) + + require.Len(t, webhookData.payloads, initialRunEventsLen+2) + + var completedRunIDs []int64 + for _, payload := range webhookData.payloads[initialRunEventsLen:] { + assert.Equal(t, "completed", payload.Action) + assert.Equal(t, "completed", payload.WorkflowRun.Status) + completedRunIDs = append(completedRunIDs, payload.WorkflowRun.ID) + } + assert.Len(t, completedRunIDs, 2) + assert.Contains(t, completedRunIDs, job1.RunID) + assert.Contains(t, completedRunIDs, job2.RunID) + + run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job1.RunID}) + run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job2.RunID}) + assert.Equal(t, actions_model.StatusFailure, run1.Status) + assert.Equal(t, actions_model.StatusFailure, run2.Status) +} + func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) { // 1. create a new webhook with special webhook for repo1 user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) From 12737883ba08f48ccd85d4f7114117be64f33baf Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 27 Mar 2026 00:53:48 +0000 Subject: [PATCH 124/207] [skip ci] Updated translations via Crowdin --- options/locale/locale_fr-FR.json | 73 +++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/options/locale/locale_fr-FR.json b/options/locale/locale_fr-FR.json index e0d6cb541e..6d0a7ccb6c 100644 --- a/options/locale/locale_fr-FR.json +++ b/options/locale/locale_fr-FR.json @@ -81,6 +81,7 @@ "retry": "Réessayez", "rerun": "Relancer", "rerun_all": "Relancer toutes les tâches", + "rerun_failed": "Relancer les tâches échouées", "save": "Enregistrer", "add": "Ajouter", "add_all": "Tout Ajouter", @@ -168,6 +169,7 @@ "search.exact_tooltip": "Inclure uniquement les résultats qui correspondent exactement au terme de recherche", "search.repo_kind": "Chercher des dépôts…", "search.user_kind": "Chercher des utilisateurs…", + "search.badge_kind": "Chercher des badges…", "search.org_kind": "Chercher des organisations…", "search.team_kind": "Chercher des équipes…", "search.code_kind": "Chercher du code…", @@ -542,6 +544,7 @@ "form.glob_pattern_error": " a un motif glob invalide : %s.", "form.regex_pattern_error": " a un motif regex invalide : %s.", "form.username_error": " ne peut contenir que des caractères alphanumériques « a-z, A-Z, 0-9 », des traits d'union « - », des tirets bas « _ » et des points « . » et ne peux ni commencer, ni finir par des symboles, ni contenir des symboles consécutifs.", + "form.invalid_slug_error": " n’est pas valide.", "form.invalid_group_team_map_error": " a une cartographie invalide : %s", "form.unknown_error": "Erreur inconnue :", "form.captcha_incorrect": "Le code CAPTCHA est incorrect.", @@ -645,6 +648,7 @@ "user.block.note.edit": "Modifier la note", "user.block.list": "Utilisateurs bloqués", "user.block.list.none": "Vous n’avez bloqué aucun utilisateur.", + "settings.general": "Général", "settings.profile": "Profil", "settings.account": "Compte", "settings.appearance": "Apparence", @@ -965,7 +969,6 @@ "repo.visibility_description": "Seuls le propriétaire ou les membres de l'organisation, s'ils ont des droits, seront en mesure de le voir.", "repo.visibility_helper": "Rendre le dépôt privé", "repo.visibility_helper_forced": "L’administrateur requière que les nouveaux dépôts soient privés.", - "repo.visibility_fork_helper": "(Changer ceci affectera toutes les bifurcations.)", "repo.clone_helper": "Besoin d'aide pour dupliquer ? Visitez l'aide.", "repo.fork_repo": "Bifurquer le dépôt", "repo.fork_from": "Bifurquer depuis", @@ -2170,7 +2173,8 @@ "repo.settings.transfer_abort_invalid": "Vous ne pouvez pas annuler un transfert de dépôt inexistant.", "repo.settings.transfer_abort_success": "Le transfert du dépôt vers %s a bien été stoppé.", "repo.settings.transfer_desc": "Transférer ce dépôt à un autre utilisateur ou une organisation dont vous possédez des droits d'administrateur.", - "repo.settings.transfer_form_title": "Entrez le nom du dépôt pour confirmer :", + "repo.settings.enter_repo_name_to_confirm": "Entrez le nom du dépôt pour confirmer :", + "repo.settings.enter_repo_full_name_to_confirm": "Entrez le nom complet du dépôt (propriétaire/nom) pour confirmer :", "repo.settings.transfer_in_progress": "Il y a actuellement un transfert en cours. Veuillez l’annuler si vous souhaitez transférer ce dépôt à un autre utilisateur.", "repo.settings.transfer_notices_1": "- Vous perdrez l'accès à ce dépôt si vous le transférez à un autre utilisateur.", "repo.settings.transfer_notices_2": "- Vous conserverez l'accès à ce dépôt si vous le transférez à une organisation dont vous êtes (co-)propriétaire.", @@ -2316,7 +2320,7 @@ "repo.settings.event_workflow_run": "Exécution du flux de travail", "repo.settings.event_workflow_run_desc": "Tâche du flux de travail Gitea Actions ajoutée, en attente, en cours ou terminée.", "repo.settings.event_workflow_job": "Tâches du flux de travail", - "repo.settings.event_workflow_job_desc": "Travaux du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée.", + "repo.settings.event_workflow_job_desc": "Tâches du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée.", "repo.settings.event_package": "Paquet", "repo.settings.event_package_desc": "Paquet créé ou supprimé.", "repo.settings.branch_filter": "Filtre de branche", @@ -2473,7 +2477,10 @@ "repo.settings.visibility.private.text": "Rendre le dépôt privé rendra non seulement le dépôt visible uniquement aux membres autorisés, mais peut également rompre la relation entre lui et ses bifurcations, observateurs, et favoris.", "repo.settings.visibility.private.bullet_title": "Changer la visibilité en privé :", "repo.settings.visibility.private.bullet_one": "Rendra le dépôt visible uniquement aux membres autorisés.", - "repo.settings.visibility.private.bullet_two": "Peut supprimer la relation avec ses bifurcations, ses observateurs et ses favoris.", + "repo.settings.visibility.private.bullet_two": "Applique la visibilité aux bifurcation et retire les observateurs et les favoris.", + "repo.settings.visibility.private.stats_stars": "Il y a %d favori(s) sur ce dépôt qui pourrai(en)t être perdu(s).", + "repo.settings.visibility.private.stats_watchers": "Il y a %d observateur(s) sur ce dépôt qui pourrai(en)t être perdu(s).", + "repo.settings.visibility.private.stats_forks": "Il y a %d bifurcation(s) associée(s) à ce dépôt.", "repo.settings.visibility.public.button": "Rendre public", "repo.settings.visibility.public.text": "Rendre le dépôt public rendra le dépôt visible à tout le monde.", "repo.settings.visibility.public.bullet_title": "Changer la visibilité en public va :", @@ -2856,6 +2863,30 @@ "admin.hooks": "Déclencheurs web", "admin.integrations": "Intégrations", "admin.authentication": "Sources d'authentification", + "admin.badges": "Badges", + "admin.badges.badges_manage_panel": "Gestion du badge", + "admin.badges.details": "Détails du badge", + "admin.badges.new_badge": "Créer un nouveau badge", + "admin.badges.slug": "Limace", + "admin.badges.slug_been_taken": "Cette limace existe déjà.", + "admin.badges.description": "Description", + "admin.badges.image_url": "URL de l’image", + "admin.badges.new_success": "Le badge « %s » a été créé.", + "admin.badges.update_success": "Le badge a été actualisé.", + "admin.badges.deletion_success": "Le badge a été supprimé.", + "admin.badges.edit_badge": "Modifier le badge", + "admin.badges.update_badge": "Mettre à jour le badge", + "admin.badges.delete_badge": "Supprimer le badge", + "admin.badges.delete_badge_desc": "Êtes-vous sûr de vouloir supprimer définitivement ce badge ?", + "admin.badges.users_with_badge": "Utilisateurs avec badge : %s", + "admin.badges.not_found": "Badge introuvable.", + "admin.badges.user_already_has": "Cet utilisateur a déjà ce badge.", + "admin.badges.user_add_success": "Le badge a bien été assigné à l‘utilisateur.", + "admin.badges.user_remove_success": "Le badge a bien été retiré de l‘utilisateur.", + "admin.badges.manage_users": "Gérer les utilisateurs", + "admin.badges.add_user": "Ajouter un utilisateur", + "admin.badges.remove_user": "Supprimer l’utilisateur", + "admin.badges.delete_user_desc": "Êtes-vous sûr de vouloir supprimer cet utilisateur du badge ?", "admin.emails": "Courriels de l’utilisateur", "admin.config": "Configuration", "admin.config_summary": "Résumé", @@ -2946,7 +2977,7 @@ "admin.dashboard.gc_lfs": "Purger les métaobjets LFS", "admin.dashboard.stop_zombie_tasks": "Arrêter les tâches zombies", "admin.dashboard.stop_endless_tasks": "Arrêter les tâches interminables", - "admin.dashboard.cancel_abandoned_jobs": "Annuler les travaux abandonnés", + "admin.dashboard.cancel_abandoned_jobs": "Annuler les actions des tâches abandonnés", "admin.dashboard.start_schedule_tasks": "Démarrer les tâches planifiées", "admin.dashboard.sync_branch.started": "Début de la synchronisation des branches", "admin.dashboard.sync_tag.started": "Synchronisation des étiquettes", @@ -3644,6 +3675,7 @@ "actions.runners.id": "ID", "actions.runners.name": "Nom", "actions.runners.owner_type": "Type", + "actions.runners.availability": "Disponibilité", "actions.runners.description": "Description", "actions.runners.labels": "Labels", "actions.runners.last_online": "Dernière fois en ligne", @@ -3659,6 +3691,12 @@ "actions.runners.update_runner": "Appliquer les modifications", "actions.runners.update_runner_success": "Exécuteur mis à jour avec succès", "actions.runners.update_runner_failed": "Impossible d'actualiser l'Exécuteur", + "actions.runners.enable_runner": "Activer cet exécuteur", + "actions.runners.enable_runner_success": "Exécuteur activé avec succès", + "actions.runners.enable_runner_failed": "Impossible d’activer l’exécuteur", + "actions.runners.disable_runner": "Désactiver cet exécuteur", + "actions.runners.disable_runner_success": "Exécuteur désactivé avec succès", + "actions.runners.disable_runner_failed": "Impossible de désactiver l’exécuteur", "actions.runners.delete_runner": "Supprimer cet exécuteur", "actions.runners.delete_runner_success": "Exécuteur supprimé avec succès", "actions.runners.delete_runner_failed": "Impossible de supprimer l'Exécuteur", @@ -3700,6 +3738,10 @@ "actions.runs.not_done": "Cette exécution du flux de travail n’est pas terminée.", "actions.runs.view_workflow_file": "Voir le fichier du flux de travail", "actions.runs.workflow_graph": "Graphique du flux", + "actions.runs.summary": "Résumé", + "actions.runs.all_jobs": "Toutes les tâches", + "actions.runs.triggered_via": "Déclenché via %s", + "actions.runs.total_duration": "Durée totale :", "actions.workflow.disable": "Désactiver le flux de travail", "actions.workflow.disable_success": "Le flux de travail « %s » a bien été désactivé.", "actions.workflow.enable": "Activer le flux de travail", @@ -3749,5 +3791,24 @@ "git.filemode.normal_file": "Fichier normal", "git.filemode.executable_file": "Fichier exécutable", "git.filemode.symbolic_link": "Lien symbolique", - "git.filemode.submodule": "Sous-module" + "git.filemode.submodule": "Sous-module", + "org.repos.none": "Aucun dépôt.", + "actions.general.permissions": "Permissions du jeton des actions", + "actions.general.token_permissions.mode": "Permissions par défaut du jeton", + "actions.general.token_permissions.mode.desc": "Une tâche d’Actions utilisera les permissions par défaut si aucune n’est déclarée dans le fichier du flux de travail.", + "actions.general.token_permissions.mode.permissive": "Permissif", + "actions.general.token_permissions.mode.permissive.desc": "Permissions en lecture et écriture sur le dépôt de la tâche.", + "actions.general.token_permissions.mode.restricted": "Restreint", + "actions.general.token_permissions.mode.restricted.desc": "Permissions en lecture seule pour le contenu (code, publications) sur le dépôt de la tâche.", + "actions.general.token_permissions.override_owner": "Écraser la configuration faite par le propriétaire", + "actions.general.token_permissions.override_owner_desc": "Si actif, ce dépôt utilisera sa propre configuration pour les actions au lieu de respecter celle du propriétaire (utilisateur ou organisation).", + "actions.general.token_permissions.maximum": "Permissions maximales du jeton", + "actions.general.token_permissions.maximum.description": "Les permissions effectives de la tâche des actions seront limitées par les permissions maximales.", + "actions.general.token_permissions.fork_pr_note": "Si une tâche est démarrée par une demande de fusion depuis une bifurcation, ses permissions effectives ne dépasseront pas les permissions en lecture-seule.", + "actions.general.token_permissions.customize_max_permissions": "Personnaliser les permissions maximales", + "actions.general.cross_repo": "Accès inter-dépôt", + "actions.general.cross_repo_desc": "Permet aux dépôts sélectionnés d’être visible en lecture-seule par tous les dépôts de ce propriétaire à l’aide de GITEA_TOKEN lors de l’exécution des tâches d’actions.", + "actions.general.cross_repo_selected": "Dépôts sélectionnés", + "actions.general.cross_repo_target_repos": "Dépôts cibles", + "actions.general.cross_repo_add": "Ajouter un dépôt cible" } From b3c69174632de10796cc0d78c1438fb8ae5e0861 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 04:39:24 +0100 Subject: [PATCH 125/207] Update JS dependencies (#37001) - Update all JS dependencies via `make update-js` - `webpack-cli` 6 to 7: remove `--disable-interpret` from Makefile - Fix lint: remove unnecessary type args, `toThrowError` to `toThrow` - Fix duplicate CSS selector detected by `stylelint` 17.6.0 - Change `updates.config.ts` to use `pin`, needed for `tailwindcss` - Pin `typescript` pending typescript-eslint/typescript-eslint#12123 --------- Co-authored-by: Claude (claude-opus-4-6) Co-authored-by: Giteabot --- Makefile | 4 +- package.json | 48 +- pnpm-lock.yaml | 2321 ++++++++--------- .../assets/img/svg/octicon-lockup-github.svg | 1 + public/assets/img/svg/octicon-logo-github.svg | 2 +- public/assets/img/svg/octicon-mark-github.svg | 2 +- updates.config.ts | 11 +- web_src/css/modules/dropdown.css | 7 +- web_src/js/features/repo-projects.ts | 4 +- web_src/js/utils.test.ts | 2 +- web_src/js/utils/dom.test.ts | 2 +- 11 files changed, 1147 insertions(+), 1257 deletions(-) create mode 100644 public/assets/img/svg/octicon-lockup-github.svg diff --git a/Makefile b/Makefile index 4d1bd96ea5..a55493ab80 100644 --- a/Makefile +++ b/Makefile @@ -382,7 +382,7 @@ watch: ## watch everything and continuously rebuild .PHONY: watch-frontend watch-frontend: node_modules ## watch frontend files and continuously rebuild @rm -rf $(WEBPACK_DEST_ENTRIES) - NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret + NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress .PHONY: watch-backend watch-backend: ## watch backend files and continuously rebuild @@ -783,7 +783,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml @$(MAKE) -s node_modules @rm -rf $(WEBPACK_DEST_ENTRIES) @echo "Running webpack..." - @BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret + @BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack @touch $(WEBPACK_DEST) .PHONY: svg diff --git a/package.json b/package.json index 4dd3f14e06..ccdb7f90a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@10.30.3", + "packageManager": "pnpm@10.33.0", "engines": { "node": ">= 22.6.0", "pnpm": ">= 10.0.0" @@ -14,8 +14,8 @@ "@github/paste-markdown": "1.5.3", "@github/text-expander-element": "2.9.4", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@mermaid-js/layout-elk": "0.2.0", - "@primer/octicons": "19.22.0", + "@mermaid-js/layout-elk": "0.2.1", + "@primer/octicons": "19.23.1", "@resvg/resvg-wasm": "2.6.2", "@silverwind/vue3-calendar-heatmap": "2.1.1", "@techknowlogick/license-checker-webpack-plugin": "0.3.0", @@ -30,7 +30,7 @@ "compare-versions": "6.1.1", "cropperjs": "1.6.2", "css-loader": "7.1.4", - "dayjs": "1.11.19", + "dayjs": "1.11.20", "dropzone": "6.0.0-beta.2", "easymde": "2.20.0", "esbuild-loader": "4.4.2", @@ -38,9 +38,9 @@ "idiomorph": "0.7.4", "jquery": "4.0.0", "js-yaml": "4.1.1", - "katex": "0.16.37", - "mermaid": "11.12.3", - "mini-css-extract-plugin": "2.10.0", + "katex": "0.16.43", + "mermaid": "11.13.0", + "mini-css-extract-plugin": "2.10.2", "monaco-editor": "0.55.1", "monaco-editor-webpack-plugin": "7.1.1", "online-3d-viewer": "0.18.0", @@ -49,25 +49,25 @@ "postcss": "8.5.8", "postcss-loader": "8.2.1", "sortablejs": "1.15.7", - "swagger-ui-dist": "5.32.0", - "tailwindcss": "3.4.17", + "swagger-ui-dist": "5.32.1", + "tailwindcss": "3.4.19", "throttle-debounce": "5.0.2", "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vue": "3.5.29", + "vue": "3.5.31", "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.3", "vue-loader": "17.4.2", "webpack": "5.105.4", - "webpack-cli": "6.0.1", + "webpack-cli": "7.0.2", "wrap-ansi": "10.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.7.1", - "@eslint/json": "1.1.0", + "@eslint/json": "1.2.0", "@playwright/test": "1.58.2", "@stylistic/eslint-plugin": "5.10.0", "@stylistic/stylelint-plugin": "5.0.1", @@ -76,16 +76,16 @@ "@types/jquery": "4.0.0", "@types/js-yaml": "4.0.9", "@types/katex": "0.16.8", - "@types/node": "25.3.5", + "@types/node": "25.5.0", "@types/pdfobject": "2.2.5", "@types/sortablejs": "1.15.9", "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.57.1", - "@vitejs/plugin-vue": "6.0.4", - "@vitest/eslint-plugin": "1.6.12", - "eslint": "10.0.3", + "@typescript-eslint/parser": "8.57.2", + "@vitejs/plugin-vue": "6.0.5", + "@vitest/eslint-plugin": "1.6.13", + "eslint": "10.1.0", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-array-func": "5.1.1", "eslint-plugin-github": "6.0.0", @@ -99,25 +99,25 @@ "eslint-plugin-vue-scoped-css": "3.0.0", "eslint-plugin-wc": "3.1.0", "globals": "17.4.0", - "happy-dom": "20.8.3", + "happy-dom": "20.8.8", "jiti": "2.6.1", "markdownlint-cli": "0.48.0", "material-icon-theme": "5.32.0", "nolyfill": "1.0.44", "postcss-html": "1.8.1", "spectral-cli-bundle": "1.0.7", - "stylelint": "17.4.0", + "stylelint": "17.6.0", "stylelint-config-recommended": "18.0.0", "stylelint-declaration-block-no-ignored-properties": "3.0.0", "stylelint-declaration-strict-value": "1.11.1", "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", "typescript": "5.9.3", - "typescript-eslint": "8.57.1", - "updates": "17.8.3", - "vite-string-plugin": "2.0.1", - "vitest": "4.0.18", - "vue-tsc": "3.2.5" + "typescript-eslint": "8.57.2", + "updates": "17.12.0", + "vite-string-plugin": "2.0.2", + "vitest": "4.1.2", + "vue-tsc": "3.2.6" }, "pnpm": { "peerDependencyRules": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bb595bf0e..4c678b0097 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,17 +51,17 @@ importers: specifier: 0.1.0-alpha-3 version: 0.1.0-alpha-3 '@mermaid-js/layout-elk': - specifier: 0.2.0 - version: 0.2.0(mermaid@11.12.3) + specifier: 0.2.1 + version: 0.2.1(mermaid@11.13.0) '@primer/octicons': - specifier: 19.22.0 - version: 19.22.0 + specifier: 19.23.1 + version: 19.23.1 '@resvg/resvg-wasm': specifier: 2.6.2 version: 2.6.2 '@silverwind/vue3-calendar-heatmap': specifier: 2.1.1 - version: 2.1.1(tippy.js@6.3.7)(vue@3.5.29(typescript@5.9.3)) + version: 2.1.1(tippy.js@6.3.7)(vue@3.5.31(typescript@5.9.3)) '@techknowlogick/license-checker-webpack-plugin': specifier: 0.3.0 version: 0.3.0(webpack@5.105.4) @@ -79,7 +79,7 @@ importers: version: 4.5.1 chartjs-adapter-dayjs-4: specifier: 1.0.4 - version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.19) + version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.20) chartjs-plugin-zoom: specifier: 2.2.0 version: 2.2.0(chart.js@4.5.1) @@ -99,8 +99,8 @@ importers: specifier: 7.1.4 version: 7.1.4(webpack@5.105.4) dayjs: - specifier: 1.11.19 - version: 1.11.19 + specifier: 1.11.20 + version: 1.11.20 dropzone: specifier: 6.0.0-beta.2 version: 6.0.0-beta.2 @@ -123,14 +123,14 @@ importers: specifier: 4.1.1 version: 4.1.1 katex: - specifier: 0.16.37 - version: 0.16.37 + specifier: 0.16.43 + version: 0.16.43 mermaid: - specifier: 11.12.3 - version: 11.12.3 + specifier: 11.13.0 + version: 11.13.0 mini-css-extract-plugin: - specifier: 2.10.0 - version: 2.10.0(webpack@5.105.4) + specifier: 2.10.2 + version: 2.10.2(webpack@5.105.4) monaco-editor: specifier: 0.55.1 version: 0.55.1 @@ -156,11 +156,11 @@ importers: specifier: 1.15.7 version: 1.15.7 swagger-ui-dist: - specifier: 5.32.0 - version: 5.32.0 + specifier: 5.32.1 + version: 5.32.1 tailwindcss: - specifier: 3.4.17 - version: 3.4.17 + specifier: 3.4.19 + version: 3.4.19 throttle-debounce: specifier: 5.0.2 version: 5.0.2 @@ -180,42 +180,42 @@ importers: specifier: 0.7.2 version: 0.7.2 vue: - specifier: 3.5.29 - version: 3.5.29(typescript@5.9.3) + specifier: 3.5.31 + version: 3.5.31(typescript@5.9.3) vue-bar-graph: specifier: 2.2.0 version: 2.2.0(typescript@5.9.3) vue-chartjs: specifier: 5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.29(typescript@5.9.3)) + version: 5.3.3(chart.js@4.5.1)(vue@3.5.31(typescript@5.9.3)) vue-loader: specifier: 17.4.2 - version: 17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4) + version: 17.4.2(vue@3.5.31(typescript@5.9.3))(webpack@5.105.4) webpack: specifier: 5.105.4 - version: 5.105.4(webpack-cli@6.0.1) + version: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-cli: - specifier: 6.0.1 - version: 6.0.1(webpack@5.105.4) + specifier: 7.0.2 + version: 7.0.2(webpack@5.105.4) wrap-ansi: specifier: 10.0.0 version: 10.0.0 devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.1 - version: 4.7.1(eslint@10.0.3(jiti@2.6.1)) + version: 4.7.1(eslint@10.1.0(jiti@2.6.1)) '@eslint/json': - specifier: 1.1.0 - version: 1.1.0 + specifier: 1.2.0 + version: 1.2.0 '@playwright/test': specifier: 1.58.2 version: 1.58.2 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@10.0.3(jiti@2.6.1)) + version: 5.10.0(eslint@10.1.0(jiti@2.6.1)) '@stylistic/stylelint-plugin': specifier: 5.0.1 - version: 5.0.1(stylelint@17.4.0(typescript@5.9.3)) + version: 5.0.1(stylelint@17.6.0(typescript@5.9.3)) '@types/codemirror': specifier: 5.60.17 version: 5.60.17 @@ -232,8 +232,8 @@ importers: specifier: 0.16.8 version: 0.16.8 '@types/node': - specifier: 25.3.5 - version: 25.3.5 + specifier: 25.5.0 + version: 25.5.0 '@types/pdfobject': specifier: 2.2.5 version: 2.2.5 @@ -250,59 +250,59 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.57.1 - version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.57.2 + version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@vitejs/plugin-vue': - specifier: 6.0.4 - version: 6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + specifier: 6.0.5 + version: 6.0.5(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3)) '@vitest/eslint-plugin': - specifier: 1.6.12 - version: 1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + specifier: 1.6.13 + version: 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))) eslint: - specifier: 10.0.3 - version: 10.0.3(jiti@2.6.1) + specifier: 10.1.0 + version: 10.1.0(jiti@2.6.1) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-array-func: specifier: 5.1.1 - version: 5.1.1(eslint@10.0.3(jiti@2.6.1)) + version: 5.1.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-de-morgan: specifier: 2.1.1 - version: 2.1.1(eslint@10.0.3(jiti@2.6.1)) + version: 2.1.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-github: specifier: 6.0.0 - version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + version: 6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-import-x: specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) + version: 4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-playwright: specifier: 2.10.1 - version: 2.10.1(eslint@10.0.3(jiti@2.6.1)) + version: 2.10.1(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-regexp: specifier: 3.1.0 - version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) + version: 3.1.0(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-sonarjs: specifier: 4.0.2 - version: 4.0.2(eslint@10.0.3(jiti@2.6.1)) + version: 4.0.2(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-unicorn: specifier: 63.0.0 - version: 63.0.0(eslint@10.0.3(jiti@2.6.1)) + version: 63.0.0(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-vue: specifier: 10.8.0 - version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))) eslint-plugin-vue-scoped-css: specifier: 3.0.0 - version: 3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@10.0.3(jiti@2.6.1)) + version: 3.1.0(eslint@10.1.0(jiti@2.6.1)) globals: specifier: 17.4.0 version: 17.4.0 happy-dom: - specifier: 20.8.3 - version: 20.8.3 + specifier: 20.8.8 + version: 20.8.8 jiti: specifier: 2.6.1 version: 2.6.1 @@ -322,20 +322,20 @@ importers: specifier: 1.0.7 version: 1.0.7 stylelint: - specifier: 17.4.0 - version: 17.4.0(typescript@5.9.3) + specifier: 17.6.0 + version: 17.6.0(typescript@5.9.3) stylelint-config-recommended: specifier: 18.0.0 - version: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) + version: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) stylelint-declaration-block-no-ignored-properties: specifier: 3.0.0 - version: 3.0.0(stylelint@17.4.0(typescript@5.9.3)) + version: 3.0.0(stylelint@17.6.0(typescript@5.9.3)) stylelint-declaration-strict-value: specifier: 1.11.1 - version: 1.11.1(stylelint@17.4.0(typescript@5.9.3)) + version: 1.11.1(stylelint@17.6.0(typescript@5.9.3)) stylelint-value-no-unknown-custom-properties: specifier: 6.1.1 - version: 6.1.1(stylelint@17.4.0(typescript@5.9.3)) + version: 6.1.1(stylelint@17.6.0(typescript@5.9.3)) svgo: specifier: 4.0.1 version: 4.0.1 @@ -343,20 +343,20 @@ importers: specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: 8.57.1 - version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.57.2 + version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) updates: - specifier: 17.8.3 - version: 17.8.3 + specifier: 17.12.0 + version: 17.12.0 vite-string-plugin: - specifier: 2.0.1 - version: 2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + specifier: 2.0.2 + version: 2.0.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) vitest: - specifier: 4.0.18 - version: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + specifier: 4.1.2 + version: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) vue-tsc: - specifier: 3.2.5 - version: 3.2.5(typescript@5.9.3) + specifier: 3.2.6 + version: 3.2.6(typescript@5.9.3) packages: @@ -379,13 +379,13 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} '@babel/types@7.29.0': @@ -477,8 +477,13 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.0': - resolution: {integrity: sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==} + '@csstools/css-syntax-patches-for-csstree@1.1.1': + resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} @@ -503,171 +508,171 @@ packages: peerDependencies: postcss-selector-parser: ^7.1.1 - '@discoveryjs/json-ext@0.6.3': - resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} + '@discoveryjs/json-ext@1.0.0': + resolution: {integrity: sha512-dDlz3W405VMFO4w5kIP9DOmELBcvFQGmLoKSdIRstBDubKFYwaNHV1NnlzMCQpXQFGWVALmeMORAuiLx18AvZQ==} engines: {node: '>=14.17.0'} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -713,16 +718,16 @@ packages: resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.4': - resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.3': - resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/json@1.1.0': - resolution: {integrity: sha512-noH9FUYqyhZSDf3Yq5HswsjDH/MWJAatMooWwT5YgQ0XHMekoFc/iyEufP+7kD1kaOj9qwFiXySqHsKii3zmlw==} + '@eslint/json@1.2.0': + resolution: {integrity: sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/object-schema@3.0.3': @@ -808,17 +813,20 @@ packages: '@mcaptcha/vanilla-glue@0.1.0-alpha-3': resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==} - '@mermaid-js/layout-elk@0.2.0': - resolution: {integrity: sha512-vjjYGnCCjYlIA/rR7M//eFi0rHM6dsMyN1JQKfckpt30DTC/esrw36hcrvA2FNPHaqh3Q/SyBWzddyaky8EtUQ==} + '@mermaid-js/layout-elk@0.2.1': + resolution: {integrity: sha512-MX9jwhMyd5zDcFsYcl3duDUkKhjVRUCGEQrdCeNV5hCIR6+3FuDDbRbFmvVbAu15K1+juzsYGG+K8MDvCY1Amg==} peerDependencies: mermaid: ^11.0.2 - '@mermaid-js/parser@1.0.0': - resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + '@mermaid-js/parser@1.0.1': + resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -890,6 +898,9 @@ packages: resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} engines: {node: '>=12.4.0'} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -905,153 +916,113 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.22.0': - resolution: {integrity: sha512-nWoh9PlE6u7xbiZF3KcUm3ktLpN2rQPt11trwp/t4EsKuYRNVWVbBp1LkCBsvZq7ScckNKUURLigIU0wS1FQdw==} + '@primer/octicons@19.23.1': + resolution: {integrity: sha512-CzjGmxkmNhyst6EekrS3SJPdtzgIkUMP/LSJch65y99/kmiFXbO1a+q7zoYe3hnI9NaOM0IN+ydDIbOmd8YqcA==} '@resvg/resvg-wasm@2.6.2': resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1213,8 +1184,8 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1261,8 +1232,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.3.5': - resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} '@types/pdfobject@2.2.5': resolution: {integrity: sha512-7gD5tqc/RUDq0PyoLemL0vEHxBYi+zY0WVaFAx/Y0jBsXFgot1vB9No1GhDZGwRGJMCIZbgAb74QG9MTyTNU/g==} @@ -1294,115 +1265,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.56.1': - resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + '@typescript-eslint/eslint-plugin@8.57.2': + resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.56.1 + '@typescript-eslint/parser': ^8.57.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/eslint-plugin@8.57.1': - resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.57.1 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.57.1': - resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} + '@typescript-eslint/parser@8.57.2': + resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + '@typescript-eslint/project-service@8.57.2': + resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.57.1': - resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + '@typescript-eslint/scope-manager@8.57.2': + resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.2': + resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.57.1': - resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.57.1': - resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.56.1': - resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + '@typescript-eslint/type-utils@8.57.2': + resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.57.1': - resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} + '@typescript-eslint/types@8.57.2': + resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.57.2': + resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.57.2': + resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.57.1': - resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/typescript-estree@8.57.1': - resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.57.1': - resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.57.1': - resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + '@typescript-eslint/visitor-keys@8.57.2': + resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -1508,54 +1427,60 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-vue@6.0.4': - resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@vitejs/plugin-vue@6.0.5': + resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.12': - resolution: {integrity: sha512-4kI47BJNFE+EQ5bmPbHzBF+ibNzx2Fj0Jo9xhWsTPxMddlHwIWl6YAxagefh461hrwx/W0QwBZpxGS404kBXyg==} + '@vitest/eslint-plugin@1.6.13': + resolution: {integrity: sha512-ui7JGWBoQpS5NKKW0FDb1eTuFEZ5EupEv2Psemuyfba7DfA5K52SeDLelt6P4pQJJ/4UGkker/BgMk/KrjH3WQ==} engines: {node: '>=18'} peerDependencies: + '@typescript-eslint/eslint-plugin': '*' eslint: '>=8.57.0' typescript: '>=5.0.0' vitest: '*' peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true typescript: optional: true vitest: optional: true - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1566,37 +1491,37 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.29': - resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + '@vue/compiler-core@3.5.31': + resolution: {integrity: sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==} - '@vue/compiler-dom@3.5.29': - resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + '@vue/compiler-dom@3.5.31': + resolution: {integrity: sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==} - '@vue/compiler-sfc@3.5.29': - resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + '@vue/compiler-sfc@3.5.31': + resolution: {integrity: sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==} - '@vue/compiler-ssr@3.5.29': - resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + '@vue/compiler-ssr@3.5.31': + resolution: {integrity: sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==} - '@vue/language-core@3.2.5': - resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==} + '@vue/language-core@3.2.6': + resolution: {integrity: sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==} - '@vue/reactivity@3.5.29': - resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + '@vue/reactivity@3.5.31': + resolution: {integrity: sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==} - '@vue/runtime-core@3.5.29': - resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + '@vue/runtime-core@3.5.31': + resolution: {integrity: sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==} - '@vue/runtime-dom@3.5.29': - resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + '@vue/runtime-dom@3.5.31': + resolution: {integrity: sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==} - '@vue/server-renderer@3.5.29': - resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + '@vue/server-renderer@3.5.31': + resolution: {integrity: sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==} peerDependencies: - vue: 3.5.29 + vue: 3.5.31 - '@vue/shared@3.5.29': - resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + '@vue/shared@3.5.31': + resolution: {integrity: sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1643,31 +1568,6 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@webpack-cli/configtest@3.0.1': - resolution: {integrity: sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/info@3.0.1': - resolution: {integrity: sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/serve@3.0.1': - resolution: {integrity: sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - webpack-dev-server: '*' - peerDependenciesMeta: - webpack-dev-server: - optional: true - '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -1793,8 +1693,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + baseline-browser-mapping@2.10.11: + resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==} engines: {node: '>=6.0.0'} hasBin: true @@ -1811,8 +1711,8 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1842,8 +1742,8 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cacheable@2.3.3: - resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==} + cacheable@2.3.4: + resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1853,8 +1753,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001777: - resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -1945,17 +1845,10 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -1988,8 +1881,11 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - core-js-compat@3.48.0: - resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} core-js@3.32.2: resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==} @@ -2212,14 +2108,14 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} - dagre-d3-es@7.0.13: - resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dayjs@1.11.19: - resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -2252,13 +2148,17 @@ packages: resolution: {integrity: sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==} engines: {node: '>=0.10.0'} - delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2288,9 +2188,8 @@ packages: dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - dompurify@3.3.2: - resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} - engines: {node: '>=20'} + dompurify@3.3.3: + resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2301,8 +2200,8 @@ packages: easymde@2.20.0: resolution: {integrity: sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==} - electron-to-chromium@1.5.307: - resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + electron-to-chromium@1.5.325: + resolution: {integrity: sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -2317,8 +2216,8 @@ packages: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} - enhanced-resolve@5.20.0: - resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -2341,9 +2240,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -2352,8 +2248,8 @@ packages: peerDependencies: webpack: ^4.40.0 || ^5.0.0 - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} hasBin: true @@ -2571,10 +2467,6 @@ packages: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@9.1.1: - resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2591,8 +2483,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.0.3: - resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + eslint@10.1.0: + resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2605,8 +2497,8 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.1.1: - resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} esquery@1.7.0: @@ -2714,15 +2606,15 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flat-cache@6.1.20: - resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flat-cache@6.1.21: + resolution: {integrity: sha512-2u7cJfSf7Th7NxEk/VzQjnPoglok2YCsevS7TSbJjcDQWJPbqUUnSYtriHSvtnq+fRZHy1s0ugk4ApnQyhPGoQ==} flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatted@3.3.4: - resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2744,8 +2636,8 @@ packages: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2799,8 +2691,8 @@ packages: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} - happy-dom@20.8.3: - resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==} + happy-dom@20.8.8: + resolution: {integrity: sha512-5/F8wxkNxYtsN0bXfMwIyNLZ9WYsoOYPbmoluqVJqv8KBUbcyKZawJ7uYK4WTX8IHBLYv+VXIwfeNDPy1oKMwQ==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -2814,13 +2706,16 @@ packages: hash-sum@2.0.0: resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - hashery@1.5.0: - resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + hookified@2.1.0: + resolution: {integrity: sha512-ootKng4eaxNxa7rx6FJv2YKef3DuhqbEj3l70oGXwddPQEEnISm50TEZQclqiLTAtilT2nu7TErtCO523hHkyg==} + html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -3049,8 +2944,8 @@ packages: just-extend@5.1.1: resolution: {integrity: sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==} - katex@0.16.37: - resolution: {integrity: sha512-TIGjO2cCGYono+uUzgkE7RFF329mLLWGuHUlSr6cwIVj9O8f0VQZ783rsanmJpFUo32vvtj7XT04NGRPh+SZFg==} + katex@0.16.43: + resolution: {integrity: sha512-K7NL5JtGrFEglipOAjY4UYA69CnTuNmjArxeXF6+bw7h2OGySUPv6QWRjfb1gmutJ4Mw/qLeBqiROOEDULp4nA==} hasBin: true keyv@4.5.4: @@ -3091,6 +2986,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3199,8 +3168,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.12.3: - resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==} + mermaid@11.13.0: + resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3289,8 +3258,8 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mini-css-extract-plugin@2.10.0: - resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==} + mini-css-extract-plugin@2.10.2: + resolution: {integrity: sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 @@ -3305,8 +3274,8 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mlly@1.8.1: - resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} monaco-editor-webpack-plugin@7.1.1: resolution: {integrity: sha512-WxdbFHS3Wtz4V9hzhe/Xog5hQRSMxmDLkEEYZwqMDHgJlkZo00HVFZR0j5d0nKypjTUkkygH3dDSXERLG4757A==} @@ -3469,12 +3438,12 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pify@2.3.0: @@ -3528,16 +3497,22 @@ packages: peerDependencies: postcss: ^8.4.21 - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} peerDependencies: + jiti: '>=1.21.0' postcss: '>=8.0.9' - ts-node: '>=9.0.0' + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: + jiti: + optional: true postcss: optional: true - ts-node: + tsx: + optional: true + yaml: optional: true postcss-loader@8.2.1: @@ -3631,8 +3606,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qified@0.6.0: - resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} + qified@0.9.0: + resolution: {integrity: sha512-4q61YgkHbY6gmwkqm0BsxyLDO3UYdrdiJTJ7JiaZb3xpW1duxn135SB7KqUEkCiuu5O4W+TtwEWP2VjmSRanvA==} engines: {node: '>=20'} queue-microtask@1.2.3: @@ -3697,12 +3672,12 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true roughjs@4.6.6: @@ -3718,8 +3693,8 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - sax@1.5.0: - resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} schema-utils@4.3.3: @@ -3739,14 +3714,14 @@ packages: engines: {node: '>=10'} hasBin: true - seroval-plugins@1.5.0: - resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} + seroval-plugins@1.5.1: + resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.5.0: - resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} + seroval@1.5.1: + resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} engines: {node: '>=10'} shallow-clone@3.0.1: @@ -3776,12 +3751,12 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - smol-toml@1.6.0: - resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} - solid-js@1.9.11: - resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} + solid-js@1.9.12: + resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} solid-transition-group@0.2.3: resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} @@ -3839,8 +3814,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -3901,8 +3876,8 @@ packages: peerDependencies: stylelint: '>=16' - stylelint@17.4.0: - resolution: {integrity: sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==} + stylelint@17.6.0: + resolution: {integrity: sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg==} engines: {node: '>=20.19.0'} hasBin: true @@ -3951,8 +3926,8 @@ packages: svgson@5.3.1: resolution: {integrity: sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==} - swagger-ui-dist@5.32.0: - resolution: {integrity: sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==} + swagger-ui-dist@5.32.1: + resolution: {integrity: sha512-6HQoo7+j8PA2QqP5kgAb9dl1uxUjvR0SAoL/WUp1sTEvm0F6D5npgU2OGCLwl++bIInqGlEUQ2mpuZRZYtyCzQ==} sync-fetch@0.4.5: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} @@ -3966,17 +3941,17 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.17: - resolution: {integrity: sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==} + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -3991,8 +3966,8 @@ packages: uglify-js: optional: true - terser@5.46.0: - resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} engines: {node: '>=10'} hasBin: true @@ -4013,16 +3988,16 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tippy.js@6.3.7: @@ -4041,8 +4016,8 @@ packages: tributejs@5.1.3: resolution: {integrity: sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -4064,8 +4039,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.57.1: - resolution: {integrity: sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==} + typescript-eslint@8.57.2: + resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4104,8 +4079,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@17.8.3: - resolution: {integrity: sha512-YTCzRy4rGdrDCuX8wrnDKn2KXYp5kJS1T94k/JKQED60+lMAxdxoasX61EG0zbWYBxQQpTvC3uAzyDu3U+2zZA==} + updates@17.12.0: + resolution: {integrity: sha512-BQvF31tGVSa79ykyonkSkS5AN91x46qZgJi0pHiIQnPH+eUkT+Xq9jIE+O0gRUKPvIDyjCmb31rn7Uf/YD6rLQ==} engines: {node: '>=22'} hasBin: true @@ -4122,20 +4097,21 @@ packages: vanilla-colorful@0.7.2: resolution: {integrity: sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==} - vite-string-plugin@2.0.1: - resolution: {integrity: sha512-L5B86yQkYrqH5d966w1vI91B0d+0vmICgB6tqjINvtBIGU9qhFY7izqjytED/ApggFC4QTDWNjfF6nWMqY/fQg==} + vite-string-plugin@2.0.2: + resolution: {integrity: sha512-pHU9lZuUoMSYyZixdn2XBYko9IAhk3dr41CG6VsXrjB+wN2th06SZsO9mJm6+2NhKBJKNfRERaRej8TBcoq9tQ==} peerDependencies: vite: '*' - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -4146,12 +4122,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -4167,20 +4145,21 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -4248,14 +4227,14 @@ packages: vue: optional: true - vue-tsc@3.2.5: - resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==} + vue-tsc@3.2.6: + resolution: {integrity: sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.29: - resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + vue@3.5.31: + resolution: {integrity: sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -4269,14 +4248,14 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-cli@6.0.1: - resolution: {integrity: sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==} - engines: {node: '>=18.12.0'} + webpack-cli@7.0.2: + resolution: {integrity: sha512-dB0R4T+C/8YuvM+fabdvil6QE44/ChDXikV5lOOkrUeCkW5hTJv2pGLE3keh+D5hjYw8icBaJkZzpFoaHV4T+g==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: - webpack: ^5.82.0 - webpack-bundle-analyzer: '*' - webpack-dev-server: '*' + webpack: ^5.101.0 + webpack-bundle-analyzer: ^4.0.0 || ^5.0.0 + webpack-dev-server: ^5.0.0 peerDependenciesMeta: webpack-bundle-analyzer: optional: true @@ -4347,8 +4326,8 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4369,11 +4348,6 @@ packages: xml-reader@2.4.3: resolution: {integrity: sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} - engines: {node: '>= 14.6'} - hasBin: true - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4385,7 +4359,7 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 '@babel/code-frame@7.29.0': dependencies: @@ -4397,11 +4371,11 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/parser@7.29.0': + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 - '@babel/runtime@7.28.6': {} + '@babel/runtime@7.29.2': {} '@babel/types@7.29.0': dependencies: @@ -4419,7 +4393,7 @@ snapshots: '@cacheable/utils@2.4.0': dependencies: - hashery: 1.5.0 + hashery: 1.5.1 keyv: 5.6.0 '@chevrotain/cst-dts-gen@11.1.2': @@ -4506,7 +4480,9 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.0': {} + '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 '@csstools/css-tokenizer@4.0.0': {} @@ -4523,120 +4499,120 @@ snapshots: dependencies: postcss-selector-parser: 7.1.1 - '@discoveryjs/json-ext@0.6.3': {} + '@discoveryjs/json-ext@1.0.0': {} - '@emnapi/core@1.8.1': + '@emnapi/core@1.9.1': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.0': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': + '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.27.4': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.1.0(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint/compat@1.4.1(eslint@10.1.0(jiti@2.6.1))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) '@eslint/config-array@0.23.3': dependencies: @@ -4658,7 +4634,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.4': + '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.14.0 debug: 4.4.3 @@ -4672,9 +4648,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.3': {} + '@eslint/js@9.39.4': {} - '@eslint/json@1.1.0': + '@eslint/json@1.2.0': dependencies: '@eslint/core': 1.1.1 '@eslint/plugin-kit': 0.6.1 @@ -4720,7 +4696,7 @@ snapshots: dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - mlly: 1.8.1 + mlly: 1.8.2 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4743,7 +4719,7 @@ snapshots: '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: - hashery: 1.5.0 + hashery: 1.5.1 hookified: 1.15.1 keyv: 5.6.0 @@ -4757,20 +4733,27 @@ snapshots: dependencies: '@mcaptcha/core-glue': 0.1.0-alpha-5 - '@mermaid-js/layout-elk@0.2.0(mermaid@11.12.3)': + '@mermaid-js/layout-elk@0.2.1(mermaid@11.13.0)': dependencies: d3: 7.9.0 elkjs: 0.9.3 - mermaid: 11.12.3 + mermaid: 11.13.0 - '@mermaid-js/parser@1.0.0': + '@mermaid-js/parser@1.0.1': dependencies: langium: 4.2.1 '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -4836,6 +4819,8 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 + '@oxc-project/types@0.122.0': {} + '@package-json/types@0.0.12': {} '@pkgr/core@0.2.9': {} @@ -4846,97 +4831,71 @@ snapshots: '@popperjs/core@2.11.8': {} - '@primer/octicons@19.22.0': + '@primer/octicons@19.23.1': dependencies: object-assign: 4.1.1 '@resvg/resvg-wasm@2.6.2': {} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rollup/rollup-android-arm-eabi@4.59.0': - optional: true - - '@rollup/rollup-android-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-x64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.59.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.59.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.59.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.59.0': - optional: true - '@rtsao/scc@1.1.0': {} '@scarf/scarf@1.4.0': {} - '@silverwind/vue3-calendar-heatmap@2.1.1(tippy.js@6.3.7)(vue@3.5.29(typescript@5.9.3))': + '@silverwind/vue3-calendar-heatmap@2.1.1(tippy.js@6.3.7)(vue@3.5.31(typescript@5.9.3))': dependencies: tippy.js: 6.3.7 - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) '@simonwep/pickr@1.9.0': dependencies: @@ -4945,32 +4904,32 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@solid-primitives/refs@1.1.3(solid-js@1.9.11)': + '@solid-primitives/refs@1.1.3(solid-js@1.9.12)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) - solid-js: 1.9.11 + '@solid-primitives/utils': 6.4.0(solid-js@1.9.12) + solid-js: 1.9.12 - '@solid-primitives/transition-group@1.1.2(solid-js@1.9.11)': + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.12)': dependencies: - solid-js: 1.9.11 + solid-js: 1.9.12 - '@solid-primitives/utils@6.4.0(solid-js@1.9.11)': + '@solid-primitives/utils@6.4.0(solid-js@1.9.12)': dependencies: - solid-js: 1.9.11 + solid-js: 1.9.12 '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/types': 8.56.1 - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/types': 8.57.2 + eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.3 + picomatch: 4.0.4 - '@stylistic/stylelint-plugin@5.0.1(stylelint@17.4.0(typescript@5.9.3))': + '@stylistic/stylelint-plugin@5.0.1(stylelint@17.6.0(typescript@5.9.3))': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -4979,7 +4938,7 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 style-search: 0.1.0 - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) '@swc/helpers@0.2.14': {} @@ -4992,7 +4951,7 @@ snapshots: spdx-expression-validate: 2.0.0 spdx-satisfies: 5.0.1 superstruct: 0.10.13 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-sources: 1.4.3 wrap-ansi: 6.2.0 @@ -5127,7 +5086,7 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 - '@types/debug@4.1.12': + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -5169,7 +5128,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.3.5': + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 @@ -5196,176 +5155,97 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/type-utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 + eslint: 10.1.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - eslint: 10.0.3(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + '@typescript-eslint/scope-manager@8.57.2': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 + + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 + eslint: 10.1.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.56.1': + '@typescript-eslint/types@8.57.2': {} + + '@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 - - '@typescript-eslint/scope-manager@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.56.1': {} - - '@typescript-eslint/types@8.57.1': {} - - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/project-service': 8.57.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3 minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + '@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/types': 8.57.2 + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/visitor-keys@8.57.2': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.56.1': - dependencies: - '@typescript-eslint/types': 8.56.1 - eslint-visitor-keys: 5.0.1 - - '@typescript-eslint/visitor-keys@8.57.1': - dependencies: - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -5427,61 +5307,69 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitejs/plugin-vue@6.0.5(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) - vue: 3.5.29(typescript@5.9.3) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) + vue: 3.5.31(typescript@5.9.3) - '@vitest/eslint-plugin@1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)))': dependencies: - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/scope-manager': 8.57.2 + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) typescript: 5.9.3 - vitest: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vitest: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.2': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.2': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.2': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.2 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.2': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.2': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.2': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@volar/language-core@2.4.28': dependencies: @@ -5495,69 +5383,69 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.29': + '@vue/compiler-core@3.5.31': dependencies: - '@babel/parser': 7.29.0 - '@vue/shared': 3.5.29 + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.31 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.29': + '@vue/compiler-dom@3.5.31': dependencies: - '@vue/compiler-core': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-core': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/compiler-sfc@3.5.29': + '@vue/compiler-sfc@3.5.31': dependencies: - '@babel/parser': 7.29.0 - '@vue/compiler-core': 3.5.29 - '@vue/compiler-dom': 3.5.29 - '@vue/compiler-ssr': 3.5.29 - '@vue/shared': 3.5.29 + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.31 + '@vue/compiler-dom': 3.5.31 + '@vue/compiler-ssr': 3.5.31 + '@vue/shared': 3.5.31 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.8 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.29': + '@vue/compiler-ssr@3.5.31': dependencies: - '@vue/compiler-dom': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/language-core@3.2.5': + '@vue/language-core@3.2.6': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.31 + '@vue/shared': 3.5.31 alien-signals: 3.1.2 muggle-string: 0.4.1 path-browserify: 1.0.1 - picomatch: 4.0.3 + picomatch: 4.0.4 - '@vue/reactivity@3.5.29': + '@vue/reactivity@3.5.31': dependencies: - '@vue/shared': 3.5.29 + '@vue/shared': 3.5.31 - '@vue/runtime-core@3.5.29': + '@vue/runtime-core@3.5.31': dependencies: - '@vue/reactivity': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/reactivity': 3.5.31 + '@vue/shared': 3.5.31 - '@vue/runtime-dom@3.5.29': + '@vue/runtime-dom@3.5.31': dependencies: - '@vue/reactivity': 3.5.29 - '@vue/runtime-core': 3.5.29 - '@vue/shared': 3.5.29 + '@vue/reactivity': 3.5.31 + '@vue/runtime-core': 3.5.31 + '@vue/shared': 3.5.31 csstype: 3.2.3 - '@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3))': + '@vue/server-renderer@3.5.31(vue@3.5.31(typescript@5.9.3))': dependencies: - '@vue/compiler-ssr': 3.5.29 - '@vue/shared': 3.5.29 - vue: 3.5.29(typescript@5.9.3) + '@vue/compiler-ssr': 3.5.31 + '@vue/shared': 3.5.31 + vue: 3.5.31(typescript@5.9.3) - '@vue/shared@3.5.29': {} + '@vue/shared@3.5.31': {} '@webassemblyjs/ast@1.14.1': dependencies: @@ -5635,21 +5523,6 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -5666,7 +5539,7 @@ snapshots: add-asset-webpack-plugin@3.1.1(webpack@5.105.4): optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: @@ -5710,7 +5583,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 arg@5.0.2: {} @@ -5722,9 +5595,9 @@ snapshots: asciinema-player@3.15.1: dependencies: - '@babel/runtime': 7.28.6 - solid-js: 1.9.11 - solid-transition-group: 0.2.3(solid-js@1.9.11) + '@babel/runtime': 7.29.2 + solid-js: 1.9.12 + solid-transition-group: 0.2.3(solid-js@1.9.12) assertion-error@2.0.1: {} @@ -5742,7 +5615,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.0: {} + baseline-browser-mapping@2.10.11: {} big.js@5.2.2: {} @@ -5755,7 +5628,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -5765,9 +5638,9 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001777 - electron-to-chromium: 1.5.307 + baseline-browser-mapping: 2.10.11 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.325 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -5784,19 +5657,19 @@ snapshots: bytes@3.1.2: {} - cacheable@2.3.3: + cacheable@2.3.4: dependencies: '@cacheable/memory': 2.0.8 '@cacheable/utils': 2.4.0 hookified: 1.15.1 keyv: 5.6.0 - qified: 0.6.0 + qified: 0.9.0 callsites@3.1.0: {} camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001777: {} + caniuse-lite@1.0.30001781: {} chai@6.2.2: {} @@ -5817,10 +5690,10 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.19): + chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.20): dependencies: chart.js: 4.5.1 - dayjs: 1.11.19 + dayjs: 1.11.20 chartjs-plugin-zoom@2.2.0(chart.js@4.5.1): dependencies: @@ -5888,12 +5761,8 @@ snapshots: colord@2.9.3: {} - colorette@2.0.20: {} - commander@11.1.0: {} - commander@12.1.0: {} - commander@14.0.3: {} commander@2.20.3: {} @@ -5912,7 +5781,9 @@ snapshots: confbox@0.1.8: {} - core-js-compat@3.48.0: + convert-source-map@2.0.0: {} + + core-js-compat@3.49.0: dependencies: browserslist: 4.28.1 @@ -5956,7 +5827,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) css-select@5.2.2: dependencies: @@ -6028,7 +5899,7 @@ snapshots: d3-delaunay@6.0.4: dependencies: - delaunator: 5.0.1 + delaunator: 5.1.0 d3-dispatch@3.0.1: {} @@ -6165,14 +6036,14 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 - dagre-d3-es@7.0.13: + dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 lodash-es: 4.17.23 damerau-levenshtein@1.0.8: {} - dayjs@1.11.19: {} + dayjs@1.11.20: {} debug@3.2.7: dependencies: @@ -6195,12 +6066,14 @@ snapshots: kind-of: 3.2.2 rename-keys: 1.2.0 - delaunator@5.0.1: + delaunator@5.1.0: dependencies: - robust-predicates: 3.0.2 + robust-predicates: 3.0.3 dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -6231,7 +6104,7 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - dompurify@3.3.2: + dompurify@3.3.3: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6254,7 +6127,7 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - electron-to-chromium@1.5.307: {} + electron-to-chromium@1.5.325: {} elkjs@0.9.3: {} @@ -6264,10 +6137,10 @@ snapshots: emojis-list@3.0.0: {} - enhanced-resolve@5.20.0: + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.2 entities@4.5.0: {} @@ -6281,46 +6154,44 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-module-lexer@1.7.0: {} - es-module-lexer@2.0.0: {} esbuild-loader@4.4.2(webpack@5.105.4): dependencies: - esbuild: 0.27.3 - get-tsconfig: 4.13.6 + esbuild: 0.27.4 + get-tsconfig: 4.13.7 loader-utils: 2.0.4 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-sources: 1.4.3 - esbuild@0.27.3: + esbuild@0.27.4: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 escalade@3.2.0: {} @@ -6328,13 +6199,13 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: - get-tsconfig: 4.13.6 + get-tsconfig: 4.13.7 stable-hash-x: 0.2.0 optionalDependencies: unrs-resolver: 1.11.1 @@ -6347,103 +6218,103 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.13.6 + get-tsconfig: 4.13.7 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.0.3(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-array-func@5.1.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-array-func@5.1.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-de-morgan@2.1.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-de-morgan@2.1.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-escompat@3.11.4(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-escompat@3.11.4(eslint@10.1.0(jiti@2.6.1)): dependencies: browserslist: 4.28.1 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-eslint-comments@3.2.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-eslint-comments@3.2.0(eslint@10.1.0(jiti@2.6.1)): dependencies: escape-string-regexp: 1.0.5 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-filenames@1.3.2(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-github@6.0.0(@types/eslint@9.6.1)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint/compat': 1.4.1(eslint@10.0.3(jiti@2.6.1)) - '@eslint/eslintrc': 3.3.4 - '@eslint/js': 9.39.3 + '@eslint/compat': 1.4.1(eslint@10.1.0(jiti@2.6.1)) + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 10.0.3(jiti@2.6.1) - eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-escompat: 3.11.4(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-filenames: 1.3.2(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.0.3(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) + eslint-config-prettier: 10.1.8(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-escompat: 3.11.4(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-filenames: 1.3.2(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-i18n-text: 1.0.1(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1) + eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(prettier@3.8.1) eslint-rule-documentation: 1.0.23 globals: 16.5.0 jsx-ast-utils: 3.3.5 prettier: 3.8.1 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-i18n-text@1.0.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)): dependencies: '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/types': 8.57.2 comment-parser: 1.4.5 debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.4 @@ -6451,12 +6322,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6465,9 +6336,9 @@ snapshots: array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.0.3(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@10.1.0(jiti@2.6.1)) hasown: '@nolyfill/hasown@1.0.44' is-core-module: '@nolyfill/is-core-module@1.0.39' is-glob: 4.0.3 @@ -6479,13 +6350,13 @@ snapshots: string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.1.0(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: '@nolyfill/array-includes@1.0.44' @@ -6495,7 +6366,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) hasown: '@nolyfill/hasown@1.0.44' jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6506,38 +6377,38 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-playwright@2.10.1(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-playwright@2.10.1(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) globals: 17.4.0 - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)))(eslint@10.0.3(jiti@2.6.1))(prettier@3.8.1): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1))(prettier@3.8.1): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) prettier: 3.8.1 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@10.0.3(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-regexp@3.1.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-regexp@3.1.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.5 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) jsdoc-type-pratt-parser: 7.1.1 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.2(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-sonarjs@4.0.2(eslint@10.1.0(jiti@2.6.1)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) functional-red-black-tree: 1.0.1 globals: 17.4.0 jsx-ast-utils-x: 0.1.0 @@ -6545,18 +6416,18 @@ snapshots: minimatch: 10.2.4 scslre: 0.3.0 semver: 7.7.4 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 - eslint-plugin-unicorn@63.0.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-unicorn@63.0.0(eslint@10.1.0(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 - core-js-compat: 3.48.0 - eslint: 10.0.3(jiti@2.6.1) + core-js-compat: 3.49.0 + eslint: 10.1.0(jiti@2.6.1) find-up-simple: 1.0.1 globals: 16.5.0 indent-string: 5.0.0 @@ -6568,33 +6439,33 @@ snapshots: semver: 7.7.4 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.0.0(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): + eslint-plugin-vue-scoped-css@3.0.0(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) lodash: 4.17.23 postcss: 8.5.8 postcss-safe-parser: 7.0.1(postcss@8.5.8) postcss-selector-parser: 7.1.1 - vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) - eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.0.3(jiti@2.6.1)))(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): + eslint-plugin-vue@10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.1.0(jiti@2.6.1)))(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - eslint: 10.0.3(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + eslint: 10.1.0(jiti@2.6.1) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) + vue-eslint-parser: 10.4.0(eslint@10.1.0(jiti@2.6.1)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-wc@3.1.0(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-wc@3.1.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 @@ -6605,13 +6476,6 @@ snapshots: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@9.1.1: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -6625,9 +6489,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.0.3(jiti@2.6.1): + eslint@10.1.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.3 '@eslint/config-helpers': 0.5.3 @@ -6643,7 +6507,7 @@ snapshots: escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 - espree: 11.1.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -6668,7 +6532,7 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - espree@11.1.1: + espree@11.2.0: dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) @@ -6724,9 +6588,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-ponyfill@7.1.0: dependencies: @@ -6738,7 +6602,7 @@ snapshots: file-entry-cache@11.1.2: dependencies: - flat-cache: 6.1.20 + flat-cache: 6.1.21 file-entry-cache@8.0.0: dependencies: @@ -6762,18 +6626,18 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.4 + flatted: 3.4.2 keyv: 4.5.4 - flat-cache@6.1.20: + flat-cache@6.1.21: dependencies: - cacheable: 2.3.3 - flatted: 3.3.4 + cacheable: 2.3.4 + flatted: 3.4.2 hookified: 1.15.1 flat@5.0.2: {} - flatted@3.3.4: {} + flatted@3.4.2: {} fs.realpath@1.0.0: {} @@ -6787,7 +6651,7 @@ snapshots: get-east-asian-width@1.5.0: {} - get-tsconfig@4.13.6: + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -6843,14 +6707,14 @@ snapshots: hammerjs@2.0.8: {} - happy-dom@20.8.3: + happy-dom@20.8.8: dependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.19.0 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6861,12 +6725,14 @@ snapshots: hash-sum@2.0.0: {} - hashery@1.5.0: + hashery@1.5.1: dependencies: hookified: 1.15.1 hookified@1.15.1: {} + hookified@2.1.0: {} + html-tags@5.1.0: {} htmlparser2@8.0.2: @@ -6984,7 +6850,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -7039,7 +6905,7 @@ snapshots: just-extend@5.1.1: {} - katex@0.16.37: + katex@0.16.43: dependencies: commander: 8.3.0 @@ -7082,6 +6948,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7147,7 +7062,7 @@ snapshots: markdownlint: 0.40.0 minimatch: 10.2.4 run-con: 1.3.2 - smol-toml: 1.6.0 + smol-toml: 1.6.1 tinyglobby: 0.2.15 transitivePeerDependencies: - supports-color @@ -7193,21 +7108,22 @@ snapshots: merge2@1.4.1: {} - mermaid@11.12.3: + mermaid@11.13.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.0 + '@mermaid-js/parser': 1.0.1 '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) cytoscape-fcose: 2.2.0(cytoscape@3.33.1) d3: 7.9.0 d3-sankey: 0.12.3 - dagre-d3-es: 7.0.13 - dayjs: 1.11.19 - dompurify: 3.3.2 - katex: 0.16.37 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.3.3 + katex: 0.16.43 khroma: 2.1.0 lodash-es: 4.17.23 marked: 16.4.2 @@ -7275,7 +7191,7 @@ snapshots: dependencies: '@types/katex': 0.16.8 devlop: 1.1.0 - katex: 0.16.37 + katex: 0.16.43 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -7368,7 +7284,7 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.12 + '@types/debug': 4.1.13 debug: 4.4.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 @@ -7391,7 +7307,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -7399,15 +7315,15 @@ snapshots: dependencies: mime-db: 1.52.0 - mini-css-extract-plugin@2.10.0(webpack@5.105.4): + mini-css-extract-plugin@2.10.2(webpack@5.105.4): dependencies: schema-utils: 4.3.3 - tapable: 2.3.0 - webpack: 5.105.4(webpack-cli@6.0.1) + tapable: 2.3.2 + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@3.1.5: dependencies: @@ -7415,7 +7331,7 @@ snapshots: minimist@1.2.8: {} - mlly@1.8.1: + mlly@1.8.2: dependencies: acorn: 8.16.0 pathe: 2.0.3 @@ -7426,7 +7342,7 @@ snapshots: dependencies: loader-utils: 2.0.4 monaco-editor: 0.55.1 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) monaco-editor@0.55.1: dependencies: @@ -7559,9 +7475,9 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pify@2.3.0: {} @@ -7574,7 +7490,7 @@ snapshots: pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.8.1 + mlly: 1.8.2 pathe: 2.0.3 playwright-core@1.58.2: {} @@ -7613,11 +7529,11 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.8 - postcss-load-config@4.0.2(postcss@8.5.8): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): dependencies: lilconfig: 3.1.3 - yaml: 2.8.2 optionalDependencies: + jiti: 1.21.7 postcss: 8.5.8 postcss-loader@8.2.1(postcss@8.5.8)(typescript@5.9.3)(webpack@5.105.4): @@ -7627,7 +7543,7 @@ snapshots: postcss: 8.5.8 semver: 7.7.4 optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) transitivePeerDependencies: - typescript @@ -7695,9 +7611,9 @@ snapshots: punycode@2.3.1: {} - qified@0.6.0: + qified@0.9.0: dependencies: - hookified: 1.15.1 + hookified: 2.1.0 queue-microtask@1.2.3: {} @@ -7707,7 +7623,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 2.3.2 rechoir@0.8.0: dependencies: @@ -7750,38 +7666,28 @@ snapshots: reusify@1.1.0: {} - robust-predicates@3.0.2: {} + robust-predicates@3.0.3: {} - rollup@4.59.0: + rolldown@1.0.0-rc.12: dependencies: - '@types/estree': 1.0.8 + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 roughjs@4.6.6: dependencies: @@ -7803,7 +7709,7 @@ snapshots: rw@1.3.3: {} - sax@1.5.0: {} + sax@1.6.0: {} schema-utils@4.3.3: dependencies: @@ -7822,11 +7728,11 @@ snapshots: semver@7.7.4: {} - seroval-plugins@1.5.0(seroval@1.5.0): + seroval-plugins@1.5.1(seroval@1.5.1): dependencies: - seroval: 1.5.0 + seroval: 1.5.1 - seroval@1.5.0: {} + seroval@1.5.1: {} shallow-clone@3.0.1: dependencies: @@ -7850,19 +7756,19 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - smol-toml@1.6.0: {} + smol-toml@1.6.1: {} - solid-js@1.9.11: + solid-js@1.9.12: dependencies: csstype: 3.2.3 - seroval: 1.5.0 - seroval-plugins: 1.5.0(seroval@1.5.0) + seroval: 1.5.1 + seroval-plugins: 1.5.1(seroval@1.5.1) - solid-transition-group@0.2.3(solid-js@1.9.11): + solid-transition-group@0.2.3(solid-js@1.9.12): dependencies: - '@solid-primitives/refs': 1.1.3(solid-js@1.9.11) - '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.11) - solid-js: 1.9.11 + '@solid-primitives/refs': 1.1.3(solid-js@1.9.12) + '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.12) + solid-js: 1.9.12 sortablejs@1.15.7: {} @@ -7912,7 +7818,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} string-width@4.2.3: dependencies: @@ -7946,29 +7852,29 @@ snapshots: style-search@0.1.0: {} - stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.6.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint-declaration-strict-value@1.11.1(stylelint@17.4.0(typescript@5.9.3)): + stylelint-declaration-strict-value@1.11.1(stylelint@17.6.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.4.0(typescript@5.9.3)): + stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.6.0(typescript@5.9.3)): dependencies: postcss-value-parser: 4.2.0 resolve: 1.22.11 - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.6.0(typescript@5.9.3) - stylelint@17.4.0(typescript@5.9.3): + stylelint@17.6.0(typescript@5.9.3): dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.0 + '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) @@ -7987,7 +7893,6 @@ snapshots: html-tags: 5.1.0 ignore: 7.0.5 import-meta-resolve: 4.2.0 - imurmurhash: 0.1.4 is-plain-object: 5.0.0 mathml-tag-names: 4.0.0 meow: 14.1.0 @@ -8050,14 +7955,14 @@ snapshots: css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 - sax: 1.5.0 + sax: 1.6.0 svgson@5.3.1: dependencies: deep-rename-keys: 0.2.1 xml-reader: 2.4.3 - swagger-ui-dist@5.32.0: + swagger-ui-dist@5.32.1: dependencies: '@scarf/scarf': 1.4.0 @@ -8080,7 +7985,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwindcss@3.4.17: + tailwindcss@3.4.19: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -8099,25 +8004,28 @@ snapshots: postcss: 8.5.8 postcss-import: 15.1.0(postcss@8.5.8) postcss-js: 4.1.0(postcss@8.5.8) - postcss-load-config: 4.0.2(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8) postcss-nested: 6.2.0(postcss@8.5.8) postcss-selector-parser: 6.1.2 resolve: 1.22.11 sucrase: 3.35.1 transitivePeerDependencies: - - ts-node + - tsx + - yaml - tapable@2.3.0: {} + tapable@2.3.2: {} - terser-webpack-plugin@5.3.17(webpack@5.105.4): + terser-webpack-plugin@5.4.0(esbuild@0.27.4)(webpack@5.105.4): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.46.0 - webpack: 5.105.4(webpack-cli@6.0.1) + terser: 5.46.1 + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) + optionalDependencies: + esbuild: 0.27.4 - terser@5.46.0: + terser@5.46.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 @@ -8138,14 +8046,14 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tippy.js@6.3.7: dependencies: @@ -8161,7 +8069,7 @@ snapshots: tributejs@5.1.3: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -8183,13 +8091,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8238,7 +8146,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - updates@17.8.3: {} + updates@17.12.0: {} uri-js@4.4.1: dependencies: @@ -8250,62 +8158,51 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vite-string-plugin@2.0.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): dependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) - vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + lightningcss: 1.32.0 + picomatch: 4.0.4 postcss: 8.5.8 - rollup: 4.59.0 + rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 + esbuild: 0.27.4 fsevents: 2.3.3 jiti: 2.6.1 - terser: 5.46.0 - yaml: 2.8.2 + terser: 5.46.1 - vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + tinyrainbow: 3.1.0 + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.3.5 - happy-dom: 20.8.3 + '@types/node': 25.5.0 + happy-dom: 20.8.8 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml vscode-jsonrpc@8.2.0: {} @@ -8326,49 +8223,49 @@ snapshots: vue-bar-graph@2.2.0(typescript@5.9.3): dependencies: - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) transitivePeerDependencies: - typescript - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.29(typescript@5.9.3)): + vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.31(typescript@5.9.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) - vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1)): + vue-eslint-parser@10.4.0(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - eslint-scope: 9.1.1 + eslint: 10.1.0(jiti@2.6.1) + eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 - espree: 11.1.1 + espree: 11.2.0 esquery: 1.7.0 semver: 7.7.4 transitivePeerDependencies: - supports-color - vue-loader@17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4): + vue-loader@17.4.2(vue@3.5.31(typescript@5.9.3))(webpack@5.105.4): dependencies: chalk: 4.1.2 hash-sum: 2.0.0 watchpack: 2.5.1 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) optionalDependencies: - vue: 3.5.29(typescript@5.9.3) + vue: 3.5.31(typescript@5.9.3) - vue-tsc@3.2.5(typescript@5.9.3): + vue-tsc@3.2.6(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.2.5 + '@vue/language-core': 3.2.6 typescript: 5.9.3 - vue@3.5.29(typescript@5.9.3): + vue@3.5.31(typescript@5.9.3): dependencies: - '@vue/compiler-dom': 3.5.29 - '@vue/compiler-sfc': 3.5.29 - '@vue/runtime-dom': 3.5.29 - '@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3)) - '@vue/shared': 3.5.29 + '@vue/compiler-dom': 3.5.31 + '@vue/compiler-sfc': 3.5.31 + '@vue/runtime-dom': 3.5.31 + '@vue/server-renderer': 3.5.31(vue@3.5.31(typescript@5.9.3)) + '@vue/shared': 3.5.31 optionalDependencies: typescript: 5.9.3 @@ -8379,21 +8276,17 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-cli@6.0.1(webpack@5.105.4): + webpack-cli@7.0.2(webpack@5.105.4): dependencies: - '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - colorette: 2.0.20 - commander: 12.1.0 + '@discoveryjs/json-ext': 1.0.0 + commander: 14.0.3 cross-spawn: 7.0.6 envinfo: 7.21.0 fastest-levenshtein: 1.0.16 import-local: 3.2.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.105.4(webpack-cli@6.0.1) + webpack: 5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2) webpack-merge: 6.0.1 webpack-merge@6.0.1: @@ -8409,7 +8302,7 @@ snapshots: webpack-sources@3.3.4: {} - webpack@5.105.4(webpack-cli@6.0.1): + webpack@5.105.4(esbuild@0.27.4)(webpack-cli@7.0.2): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -8421,7 +8314,7 @@ snapshots: acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.20.0 + enhanced-resolve: 5.20.1 es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -8432,12 +8325,12 @@ snapshots: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.17(webpack@5.105.4) + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(esbuild@0.27.4)(webpack@5.105.4) watchpack: 2.5.1 webpack-sources: 3.3.4 optionalDependencies: - webpack-cli: 6.0.1(webpack@5.105.4) + webpack-cli: 7.0.2(webpack@5.105.4) transitivePeerDependencies: - '@swc/core' - esbuild @@ -8485,7 +8378,7 @@ snapshots: dependencies: signal-exit: 4.1.0 - ws@8.19.0: {} + ws@8.20.0: {} xml-lexer@0.2.2: dependencies: @@ -8498,6 +8391,4 @@ snapshots: eventemitter3: 2.0.3 xml-lexer: 0.2.2 - yaml@2.8.2: {} - yocto-queue@0.1.0: {} diff --git a/public/assets/img/svg/octicon-lockup-github.svg b/public/assets/img/svg/octicon-lockup-github.svg new file mode 100644 index 0000000000..746317496c --- /dev/null +++ b/public/assets/img/svg/octicon-lockup-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-logo-github.svg b/public/assets/img/svg/octicon-logo-github.svg index 8aae451ae5..cd09f6ac14 100644 --- a/public/assets/img/svg/octicon-logo-github.svg +++ b/public/assets/img/svg/octicon-logo-github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-mark-github.svg b/public/assets/img/svg/octicon-mark-github.svg index 6d6dc40886..a46d882513 100644 --- a/public/assets/img/svg/octicon-mark-github.svg +++ b/public/assets/img/svg/octicon-mark-github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/updates.config.ts b/updates.config.ts index a7eb364b44..afe0f8e04a 100644 --- a/updates.config.ts +++ b/updates.config.ts @@ -1,9 +1,10 @@ import type {Config} from 'updates'; export default { - exclude: [ - '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled - 'cropperjs', // need to migrate to v2 but v2 is not compatible with v1 - 'tailwindcss', // need to migrate - ], + pin: { + '@mcaptcha/vanilla-glue': '^0.1', // breaking changes in rc versions need to be handled + 'cropperjs': '^1', // need to migrate to v2 but v2 is not compatible with v1 + 'tailwindcss': '^3', // need to migrate + 'typescript': '^5', // wait on https://github.com/typescript-eslint/typescript-eslint/issues/12123 + }, } satisfies Config; diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css index 1c6e7f8552..62ca91e61f 100644 --- a/web_src/css/modules/dropdown.css +++ b/web_src/css/modules/dropdown.css @@ -274,6 +274,8 @@ select.ui.dropdown { .ui.selection.active.dropdown { border-color: var(--color-primary); box-shadow: 0 6px 18px var(--color-shadow); + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; } .ui.selection.active.dropdown .menu { @@ -311,11 +313,6 @@ select.ui.dropdown { z-index: 3; } -.ui.active.selection.dropdown { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - .ui.active.empty.selection.dropdown { border-radius: 0.28571429rem !important; box-shadow: none !important; diff --git a/web_src/js/features/repo-projects.ts b/web_src/js/features/repo-projects.ts index 1b1b4e2d24..2432d1b035 100644 --- a/web_src/js/features/repo-projects.ts +++ b/web_src/js/features/repo-projects.ts @@ -129,11 +129,11 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void { const textColor = contrastColor(elColumnColor.value); elBoardColumn.style.setProperty('background', elColumnColor.value, 'important'); elBoardColumn.style.setProperty('color', textColor, 'important'); - queryElemChildren(elBoardColumn, '.divider', (divider) => divider.style.color = textColor); + queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.color = textColor); } else { elBoardColumn.style.removeProperty('background'); elBoardColumn.style.removeProperty('color'); - queryElemChildren(elBoardColumn, '.divider', (divider) => divider.style.removeProperty('color')); + queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.removeProperty('color')); } fomanticQuery(elModal).modal('hide'); diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index edfc763148..f041a2ceca 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -114,7 +114,7 @@ test('toAbsoluteUrl', () => { expect(toAbsoluteUrl('')).toEqual('http://localhost:3000'); expect(toAbsoluteUrl('/user/repo')).toEqual('http://localhost:3000/user/repo'); - expect(() => toAbsoluteUrl('path')).toThrowError('unsupported'); + expect(() => toAbsoluteUrl('path')).toThrow('unsupported'); }); test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts index 61361e0168..3edbe94ce4 100644 --- a/web_src/js/utils/dom.test.ts +++ b/web_src/js/utils/dom.test.ts @@ -34,7 +34,7 @@ test('querySingleVisibleElem', () => { el = createElementFromHTML('
    foobar
    '); expect(querySingleVisibleElem(el, 'span')!.textContent).toEqual('bar'); el = createElementFromHTML('
    foobar
    '); - expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element'); + expect(() => querySingleVisibleElem(el, 'span')).toThrow('Expected exactly one visible element'); }); test('queryElemChildren', () => { From de478c4b6f14a5cd745dc8234f55be859b444de1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 11:49:11 +0100 Subject: [PATCH 126/207] Add e2e tests for server push events (#36879) Add e2e tests for the three server push features: - **Notification count**: verifies badge appears when another user creates an issue - **Stopwatch**: verifies stopwatch element is rendered when a stopwatch is active - **Logout propagation**: verifies logout in one tab triggers redirect in another Tests are transport-agnostic in preparation for a future WebSocket migration. --------- Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- Makefile | 2 +- tests/e2e/events.test.ts | 83 +++++++++++++++++++++++++++++ tests/e2e/register.test.ts | 7 +-- tests/e2e/utils.ts | 62 ++++++++++++++++++--- tools/test-e2e.sh | 3 ++ web_src/js/features/notification.ts | 57 +++----------------- web_src/js/features/stopwatch.ts | 56 +++---------------- web_src/js/modules/worker.ts | 68 ++++++++++++++++++++--- 8 files changed, 222 insertions(+), 116 deletions(-) create mode 100644 tests/e2e/events.test.ts diff --git a/Makefile b/Makefile index a55493ab80..5ca1c0eda6 100644 --- a/Makefile +++ b/Makefile @@ -672,7 +672,7 @@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) endif CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ -$(EXECUTABLE_E2E): $(GO_SOURCES) +$(EXECUTABLE_E2E): $(GO_SOURCES) $(WEBPACK_DEST) CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ .PHONY: release diff --git a/tests/e2e/events.test.ts b/tests/e2e/events.test.ts new file mode 100644 index 0000000000..61f1a3c881 --- /dev/null +++ b/tests/e2e/events.test.ts @@ -0,0 +1,83 @@ +import {test, expect} from '@playwright/test'; +import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiDeleteUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch} from './utils.ts'; + +// These tests rely on a short EVENT_SOURCE_UPDATE_TIME in the e2e server config. +test.describe('events', () => { + test('notification count', async ({page, request}) => { + const id = `ev-notif-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const owner = `${id}-owner`; + const commenter = `${id}-commenter`; + const repoName = id; + + await Promise.all([apiCreateUser(request, owner), apiCreateUser(request, commenter)]); + + // Create repo and login in parallel — repo is needed for the issue, login for the event stream + await Promise.all([ + apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}), + loginUser(page, owner), + ]); + const badge = page.locator('a.not-mobile .notification_count'); + await expect(badge).toBeHidden(); + + // Create issue as another user — this generates a notification delivered via server push + await apiCreateIssue(request, owner, repoName, {title: 'events notification test', headers: apiUserHeaders(commenter)}); + + // Wait for the notification badge to appear via server event + await expect(badge).toBeVisible({timeout: 15000}); + + // Cleanup + await Promise.all([apiDeleteUser(request, commenter), apiDeleteUser(request, owner)]); + }); + + test('stopwatch', async ({page, request}) => { + const name = `ev-sw-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const headers = apiUserHeaders(name); + + await apiCreateUser(request, name); + + // Create repo, issue, and start stopwatch before login + await apiCreateRepo(request, {name, headers}); + await apiCreateIssue(request, name, name, {title: 'events stopwatch test', headers}); + await apiStartStopwatch(request, name, name, 1, {headers}); + + // Login — page renders with the active stopwatch element + await loginUser(page, name); + + // Verify stopwatch is visible and links to the correct issue + const stopwatch = page.locator('.active-stopwatch.not-mobile'); + await expect(stopwatch).toBeVisible(); + + // Cleanup + await apiDeleteUser(request, name); + }); + + test('logout propagation', async ({browser, request}) => { + const name = `ev-logout-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + + await apiCreateUser(request, name); + + // Use a single context so both pages share the same session and SharedWorker + const context = await browser.newContext({baseURL: baseUrl()}); + const page1 = await context.newPage(); + const page2 = await context.newPage(); + + await loginUser(page1, name); + + // Navigate page2 so it connects to the shared event stream + await page2.goto('/'); + + // Verify page2 is logged in + await expect(page2.getByRole('link', {name: 'Sign In'})).toBeHidden(); + + // Logout from page1 — this sends a logout event to all tabs + await page1.goto('/user/logout'); + + // page2 should be redirected via the logout event + await expect(page2.getByRole('link', {name: 'Sign In'})).toBeVisible(); + + await context.close(); + + // Cleanup + await apiDeleteUser(request, name); + }); +}); diff --git a/tests/e2e/register.test.ts b/tests/e2e/register.test.ts index 425fc7e40c..5c70541747 100644 --- a/tests/e2e/register.test.ts +++ b/tests/e2e/register.test.ts @@ -1,6 +1,6 @@ import {env} from 'node:process'; import {test, expect} from '@playwright/test'; -import {login, logout} from './utils.ts'; +import {login, logout, apiDeleteUser} from './utils.ts'; test.beforeEach(async ({page}) => { await page.goto('/user/sign_up'); @@ -50,10 +50,7 @@ test('register then login', async ({page}) => { await login(page, username, password); // delete via API because of issues related to form-fetch-action - const response = await page.request.delete(`/api/v1/admin/users/${username}?purge=true`, { - headers: {Authorization: `Basic ${btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`}, - }); - expect(response.ok()).toBeTruthy(); + await apiDeleteUser(page.request, username); }); test('register with existing username shows error', async ({page}) => { diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 6ee16b32f8..aded858600 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -1,13 +1,18 @@ +import {randomBytes} from 'node:crypto'; import {env} from 'node:process'; import {expect} from '@playwright/test'; import type {APIRequestContext, Locator, Page} from '@playwright/test'; -export function apiBaseUrl() { +export function baseUrl() { return env.GITEA_TEST_E2E_URL?.replace(/\/$/g, ''); } +function apiAuthHeader(username: string, password: string) { + return {Authorization: `Basic ${globalThis.btoa(`${username}:${password}`)}`}; +} + export function apiHeaders() { - return {Authorization: `Basic ${globalThis.btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`}; + return apiAuthHeader(env.GITEA_TEST_E2E_USER, env.GITEA_TEST_E2E_PASSWORD); } async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => number; text: () => Promise}>, label: string) { @@ -24,30 +29,73 @@ async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => numb } } -export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true}: {name: string; autoInit?: boolean}) { - await apiRetry(() => requestContext.post(`${apiBaseUrl()}/api/v1/user/repos`, { - headers: apiHeaders(), +export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true, headers}: {name: string; autoInit?: boolean; headers?: Record}) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/user/repos`, { + headers: headers || apiHeaders(), data: {name, auto_init: autoInit}, }), 'apiCreateRepo'); } +export async function apiCreateIssue(requestContext: APIRequestContext, owner: string, repo: string, {title, headers}: {title: string; headers?: Record}) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues`, { + headers: headers || apiHeaders(), + data: {title}, + }), 'apiCreateIssue'); +} + +export async function apiStartStopwatch(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, {headers}: {headers?: Record} = {}) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}/stopwatch/start`, { + headers: headers || apiHeaders(), + }), 'apiStartStopwatch'); +} + export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) { - await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/repos/${owner}/${name}`, { + await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, { headers: apiHeaders(), }), 'apiDeleteRepo'); } export async function apiDeleteOrg(requestContext: APIRequestContext, name: string) { - await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/orgs/${name}`, { + await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/orgs/${name}`, { headers: apiHeaders(), }), 'apiDeleteOrg'); } +/** Generate a random password that satisfies the complexity requirements. */ +function generatePassword() { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + return `${Array.from(randomBytes(12), (b) => chars[b % chars.length]).join('')}!aA1`; +} + +/** Random password shared by all test users — used for both API user creation and browser login. */ +const testUserPassword = generatePassword(); + +export function apiUserHeaders(username: string) { + return apiAuthHeader(username, testUserPassword); +} + +export async function apiCreateUser(requestContext: APIRequestContext, username: string) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/admin/users`, { + headers: apiHeaders(), + data: {username, password: testUserPassword, email: `${username}@${env.GITEA_TEST_E2E_DOMAIN}`, must_change_password: false}, + }), 'apiCreateUser'); +} + +export async function apiDeleteUser(requestContext: APIRequestContext, username: string) { + await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/admin/users/${username}?purge=true`, { + headers: apiHeaders(), + }), 'apiDeleteUser'); +} + export async function clickDropdownItem(page: Page, trigger: Locator, itemText: string) { await trigger.click(); await page.getByText(itemText).click(); } +export async function loginUser(page: Page, username: string) { + return login(page, username, testUserPassword); +} + export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) { await page.goto('/user/login'); await page.getByLabel('Username or Email Address').fill(username); diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh index d8608a85bb..1ee513c109 100755 --- a/tools/test-e2e.sh +++ b/tools/test-e2e.sh @@ -34,6 +34,9 @@ INSTALL_LOCK = true [service] ENABLE_CAPTCHA = false +[ui.notification] +EVENT_SOURCE_UPDATE_TIME = 500ms + [log] MODE = console LEVEL = Warn diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index 915f65f88d..acb1b68f28 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -1,8 +1,8 @@ import {GET} from '../modules/fetch.ts'; import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; -import {logoutFromWorker} from '../modules/worker.ts'; +import {UserEventsSharedWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings} = window.config; let notificationSequenceNumber = 0; async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { @@ -33,56 +33,15 @@ export function initNotificationCount() { if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); - worker.addEventListener('error', (event) => { - console.error('worker error', event); - }); - worker.port.addEventListener('messageerror', () => { - console.error('unable to deserialize message'); - }); - worker.port.postMessage({ - type: 'start', - url: `${window.location.origin}${appSubUrl}/user/events`, - }); - worker.port.addEventListener('message', (event: MessageEvent<{type: string, data: string}>) => { - if (!event.data || !event.data.type) { - console.error('unknown worker message event', event); - return; - } - if (event.data.type === 'notification-count') { - receiveUpdateCount(event); // no await - } else if (event.data.type === 'no-event-source') { - // browser doesn't support EventSource, falling back to periodic poller + const worker = new UserEventsSharedWorker('notification-worker'); + worker.addMessageEventListener((event: MessageEvent) => { + if (event.data.type === 'no-event-source') { if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); - } else if (event.data.type === 'error') { - console.error('worker port event error', event.data); - } else if (event.data.type === 'logout') { - if (event.data.data !== 'here') { - return; - } - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - logoutFromWorker(); - } else if (event.data.type === 'close') { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); + } else if (event.data.type === 'notification-count') { + receiveUpdateCount(event); // no await } }); - worker.port.addEventListener('error', (e) => { - console.error('worker port error', e); - }); - worker.port.start(); - window.addEventListener('beforeunload', () => { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - }); - + worker.startPort(); return; } diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index 34e985332b..6fa8fbbdf3 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -1,9 +1,9 @@ import {createTippy} from '../modules/tippy.ts'; import {GET} from '../modules/fetch.ts'; import {hideElem, queryElems, showElem} from '../utils/dom.ts'; -import {logoutFromWorker} from '../modules/worker.ts'; +import {UserEventsSharedWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings, enableTimeTracking} = window.config; export function initStopwatch() { if (!enableTimeTracking) { @@ -47,56 +47,16 @@ export function initStopwatch() { // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); - worker.addEventListener('error', (event) => { - console.error('worker error', event); - }); - worker.port.addEventListener('messageerror', () => { - console.error('unable to deserialize message'); - }); - worker.port.postMessage({ - type: 'start', - url: `${window.location.origin}${appSubUrl}/user/events`, - }); - worker.port.addEventListener('message', (event) => { - if (!event.data || !event.data.type) { - console.error('unknown worker message event', event); - return; - } - if (event.data.type === 'stopwatches') { - updateStopwatchData(JSON.parse(event.data.data)); - } else if (event.data.type === 'no-event-source') { + const worker = new UserEventsSharedWorker('stopwatch-worker'); + worker.addMessageEventListener((event) => { + if (event.data.type === 'no-event-source') { // browser doesn't support EventSource, falling back to periodic poller if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); - } else if (event.data.type === 'error') { - console.error('worker port event error', event.data); - } else if (event.data.type === 'logout') { - if (event.data.data !== 'here') { - return; - } - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - logoutFromWorker(); - } else if (event.data.type === 'close') { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); + } else if (event.data.type === 'stopwatches') { + updateStopwatchData(JSON.parse(event.data.data)); } }); - worker.port.addEventListener('error', (e) => { - console.error('worker port error', e); - }); - worker.port.start(); - window.addEventListener('beforeunload', () => { - worker.port.postMessage({ - type: 'close', - }); - worker.port.close(); - }); - + worker.startPort(); return; } diff --git a/web_src/js/modules/worker.ts b/web_src/js/modules/worker.ts index af2e52f411..b730e30bb2 100644 --- a/web_src/js/modules/worker.ts +++ b/web_src/js/modules/worker.ts @@ -1,9 +1,65 @@ -import {sleep} from '../utils.ts'; +const {appSubUrl, assetVersionEncoded} = window.config; -const {appSubUrl} = window.config; +export class UserEventsSharedWorker { + sharedWorker: SharedWorker; -export async function logoutFromWorker(): Promise { - // wait for a while because other requests (eg: logout) may be in the flight - await sleep(5000); - window.location.href = `${appSubUrl}/`; + // options can be either a string (the debug name of the worker) or an object of type WorkerOptions + constructor(options?: string | WorkerOptions) { + const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, options); + this.sharedWorker = worker; + worker.addEventListener('error', (event) => { + console.error('worker error', event); + }); + worker.port.addEventListener('messageerror', () => { + console.error('unable to deserialize message'); + }); + worker.port.postMessage({ + type: 'start', + url: `${window.location.origin}${appSubUrl}/user/events`, + }); + worker.port.addEventListener('error', (e) => { + console.error('worker port error', e); + }); + window.addEventListener('beforeunload', () => { + // FIXME: this logic is not quite right. + // "beforeunload" can be canceled by some actions like "are-you-sure" and the navigation can be cancelled. + // In this case: the worker port is incorrectly closed while the page is still there. + worker.port.postMessage({type: 'close'}); + worker.port.close(); + }); + } + + addMessageEventListener(listener: (event: MessageEvent) => void) { + this.sharedWorker.port.addEventListener('message', (event: MessageEvent) => { + if (!event.data || !event.data.type) { + console.error('unknown worker message event', event); + return; + } + + if (event.data.type === 'error') { + console.error('worker port event error', event.data); + } else if (event.data.type === 'logout') { + if (event.data.data !== 'here') return; + this.sharedWorker.port.postMessage({type: 'close'}); + this.sharedWorker.port.close(); + // slightly delay our "logout" for a short while, in case there are other logout requests in-flight. + // * if the logout is triggered by a page redirection (e.g.: user clicks "/user/logout") + // * "beforeunload" event is triggered, this code path won't execute + // * if the logout is triggered by a fetch call + // * "beforeunload" event is not triggered until JS does the redirection. + // * in this case, the logout fetch call already completes and has sent the "logout" message to the worker + // * there can be a data-race between the fetch call's redirection and the "logout" message from the worker + // * the fetch call's logout redirection should always win over the worker message, because it might have a custom location + setTimeout(() => { window.location.href = `${appSubUrl}/` }, 1000); + } else if (event.data.type === 'close') { + this.sharedWorker.port.postMessage({type: 'close'}); + this.sharedWorker.port.close(); + } + listener(event); + }); + } + + startPort() { + this.sharedWorker.port.start(); + } } From 74c40d46ee5da5cffa25e9e6d3780a1e24145285 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Sat, 28 Mar 2026 00:38:40 +0100 Subject: [PATCH 127/207] add missing cron tasks to example ini (#37012) closes: https://github.com/go-gitea/gitea/issues/37009 docs PR: https://gitea.com/gitea/docs/pulls/371 --- custom/conf/app.example.ini | 92 +++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b752a81ca9..4df50f5cc6 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2276,6 +2276,22 @@ LEVEL = Info ;; Unreferenced blobs created more than OLDER_THAN ago are subject to deletion ;OLDER_THAN = 24h +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Synchronize repository licenses +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.sync_repo_licenses] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Whether to enable the job +;ENABLED = false +;; Whether to always run at least once at start up time (if ENABLED) +;RUN_AT_START = false +;; Whether to emit notice on successful execution too +;NOTICE_ON_SUCCESS = false +;; Time interval for job to run +;SCHEDULE = @annually + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2337,6 +2353,18 @@ LEVEL = Info ;NOTICE_ON_SUCCESS = false ;SCHEDULE = @every 72h +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Update the '.ssh/authorized_principals' file +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.resync_all_sshprincipals] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = false +;RUN_AT_START = false +;NOTICE_ON_SUCCESS = false +;SCHEDULE = @every 72h + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...) @@ -2445,6 +2473,70 @@ LEVEL = Info ;Check at least this proportion of LFSMetaObjects per repo. (This may cause all stale LFSMetaObjects to be checked.) ;PROPORTION_TO_CHECK_PER_REPO = 0.6 +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Rebuild issue index +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.rebuild_issue_indexer] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = false +;RUN_AT_START = false +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @annually + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Actions cron tasks +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Stop running tasks which haven't been updated for a long time +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.stop_zombie_tasks] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = true +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 5m + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Stop running tasks which have running status and continuous updates but don't end for a long time +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.stop_endless_tasks] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = true +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 30m + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Cancel jobs which haven't been picked up for a long time +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.cancel_abandoned_jobs] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = false +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 6h + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Start cron based actions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[cron.start_schedule_tasks] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;ENABLED = true +;RUN_AT_START = false +;NO_SUCCESS_NOTICE = false +;SCHEDULE = @every 1m + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[mirror] From 17b802beae1928049319f7ea02b5fa9db6ec0b85 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 28 Mar 2026 08:59:52 +0100 Subject: [PATCH 128/207] Clean up checkbox cursor styles (#37016) 1. Remove non-functional `label:enabled` selector (`:enabled` only works on [form controls](https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled), not labels) 2. Remove `cursor: auto` which caused an I-beam text selection cursor on checkbox labels. The default browser styles work find and show regular cursor. 3. Remove `cursor: pointer` on checkbox itself, opinionated and not needed. Co-authored-by: Claude (Opus 4.6) --- web_src/css/modules/checkbox.css | 7 ------- 1 file changed, 7 deletions(-) diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css index 220abfc17d..f24b91df07 100644 --- a/web_src/css/modules/checkbox.css +++ b/web_src/css/modules/checkbox.css @@ -91,14 +91,7 @@ input[type="checkbox"]:indeterminate::before { height: var(--checkbox-size); } -.ui.checkbox input[type="checkbox"]:enabled, -.ui.checkbox input[type="radio"]:enabled, -.ui.checkbox label:enabled { - cursor: pointer; -} - .ui.checkbox label { - cursor: auto; position: relative; display: block; } From 896e4838cbb367b0874be773a489a5a304f0c8d0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 28 Mar 2026 10:05:56 +0100 Subject: [PATCH 129/207] Update message severity colors, fix navbar double border (#37019) - Tweak serverity background and border colors - Use default text color instead of per-severity text colors. - Replace `saturate` filter with semibold font weight on message headers. - Fix navbar double border when a notification is present. Co-authored-by: Claude (Opus 4.6) --- web_src/css/modules/message.css | 2 +- web_src/css/modules/navbar.css | 5 +++++ web_src/css/themes/theme-gitea-dark.css | 28 ++++++++++++------------ web_src/css/themes/theme-gitea-light.css | 28 ++++++++++++------------ 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css index ce997c4350..d5346616bc 100644 --- a/web_src/css/modules/message.css +++ b/web_src/css/modules/message.css @@ -43,7 +43,7 @@ .ui.message .header { color: inherit; - filter: saturate(2); + font-weight: var(--font-weight-semibold); } .ui.info.message, diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css index 19a9f389d7..7a55f80fee 100644 --- a/web_src/css/modules/navbar.css +++ b/web_src/css/modules/navbar.css @@ -7,6 +7,11 @@ padding: 0 10px; } +/* When notification message is present after navbar, hide border to avoid double border */ +#navbar:has(+ .ui.message) { + border-bottom: none; +} + #navbar .navbar-left, #navbar .navbar-right { display: flex; diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index fbdef1e2fb..610e5f1344 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -162,20 +162,20 @@ gitea-theme-meta-info { --color-diff-removed-row-border: #634343; --color-diff-removed-word-bg: #6f3333; --color-diff-inactive: #22282d; - --color-error-border: #da3633; - --color-error-bg: #3c2425; - --color-error-bg-active: #5a3637; - --color-error-bg-hover: #4c2d2e; - --color-error-text: #f5817c; - --color-success-border: #458a57; - --color-success-bg: #284034; - --color-success-text: #69be61; - --color-warning-border: #9e6a03; - --color-warning-bg: #2f2a1b; - --color-warning-text: #d29922; - --color-info-border: #306090; - --color-info-bg: #26354c; - --color-info-text: #48b7f8; + --color-error-border: #763232; + --color-error-bg: #322226; + --color-error-bg-active: #49262a; + --color-error-bg-hover: #3c2427; + --color-error-text: var(--color-text); + --color-success-border: #225633; + --color-success-bg: #1c3329; + --color-success-text: var(--color-text); + --color-warning-border: #5f481a; + --color-warning-bg: #342e1f; + --color-warning-text: var(--color-text); + --color-info-border: #254a7e; + --color-info-bg: #1b283a; + --color-info-text: var(--color-text); --color-red-badge: #db2828; --color-red-badge-bg: #db28281a; --color-red-badge-hover-bg: #db28284d; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 761cb18da0..0885c5618b 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -162,20 +162,20 @@ gitea-theme-meta-info { --color-diff-removed-row-border: #f1c0c0; --color-diff-removed-word-bg: #fdb8c0; --color-diff-inactive: #f0f2f4; - --color-error-border: #d63333; - --color-error-bg: #ffebeb; - --color-error-bg-active: #fdd; - --color-error-bg-hover: #fee; - --color-error-text: #8a3231; - --color-success-border: #49842b; - --color-success-bg: #eef6e4; - --color-success-text: #2f6e30; - --color-warning-border: #bf8700; - --color-warning-bg: #fff8e1; - --color-warning-text: #744500; - --color-info-border: #2d8fa8; - --color-info-bg: #e8f4fd; - --color-info-text: #216078; + --color-error-border: #ff818266; + --color-error-bg: #ffebe9; + --color-error-bg-active: #ffcecb; + --color-error-bg-hover: #ffdcd7; + --color-error-text: var(--color-text); + --color-success-border: #4ac26b66; + --color-success-bg: #dafbe1; + --color-success-text: var(--color-text); + --color-warning-border: #d4a72c66; + --color-warning-bg: #fff8c5; + --color-warning-text: var(--color-text); + --color-info-border: #54aeff66; + --color-info-bg: #ddf4ff; + --color-info-text: var(--color-text); --color-red-badge: #db2828; --color-red-badge-bg: #db28281a; --color-red-badge-hover-bg: #db28284d; From b136a66d123a2a7d456775aeccc50086a3432ea2 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 28 Mar 2026 10:41:34 +0100 Subject: [PATCH 130/207] Restyle Workflow Graph (#36912) Follow GitHub's style and fine tune colors & layouts. Co-authored-by: Claude Sonnet 4.6 Co-authored-by: wxiaoguang Co-authored-by: silverwind --- routers/web/devtest/mock_actions.go | 27 +- routers/web/repo/actions/view.go | 2 + web_src/css/base.css | 4 +- web_src/css/themes/theme-gitea-dark.css | 2 +- web_src/css/themes/theme-gitea-light.css | 2 +- .../js/components/ActionRunSummaryView.vue | 39 +- web_src/js/components/ActionRunView.ts | 1 + web_src/js/components/RepoActionView.vue | 13 +- web_src/js/components/WorkflowGraph.vue | 777 +++++++----------- web_src/js/features/repo-actions.ts | 6 +- web_src/js/modules/gitea-actions.ts | 1 + 11 files changed, 385 insertions(+), 489 deletions(-) diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 00ca095e71..0fb2a35824 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -68,6 +68,7 @@ func MockActionsRunsJobs(ctx *context.Context) { runID := ctx.PathParamInt64("run") resp := &actions.ViewResponse{} + resp.State.Run.RepoID = 12345 resp.State.Run.TitleHTML = `mock run title link` resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10) resp.State.Run.Status = actions_model.StatusRunning.String() @@ -135,12 +136,36 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID*10 + 2, JobID: "job-102", - Name: "job 102", + Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit", Status: actions_model.StatusFailure.String(), CanRerun: false, Duration: "3h", Needs: []string{"job-100", "job-101"}, }) + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ + ID: runID*10 + 3, + JobID: "job-103", + Name: "job 103", + Status: actions_model.StatusCancelled.String(), + CanRerun: false, + Duration: "2m", + Needs: []string{"job-100"}, + }) + + // add more jobs to a run for UI testing + if resp.State.Run.CanCancel { + for i := range 10 { + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ + ID: runID*1000 + int64(i), + JobID: "job-dup-test-" + strconv.Itoa(i), + Name: "job dup test " + strconv.Itoa(i), + Status: actions_model.StatusSuccess.String(), + CanRerun: false, + Duration: "2m", + Needs: []string{"job-103", "job-101", "job-100"}, + }) + } + } fillViewRunResponseCurrentJob(ctx, resp) ctx.JSON(http.StatusOK, resp) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 90810a6d25..6b3e95f3da 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -129,6 +129,7 @@ type ViewResponse struct { State struct { Run struct { + RepoID int64 `json:"repoId"` Link string `json:"link"` Title string `json:"title"` TitleHTML template.HTML `json:"titleHTML"` @@ -252,6 +253,7 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, return } + resp.State.Run.RepoID = ctx.Repo.Repository.ID // the title for the "run" is from the commit message resp.State.Run.Title = run.Title resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository) diff --git a/web_src/css/base.css b/web_src/css/base.css index b4139c0e72..60317887ba 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -808,9 +808,7 @@ table th[data-sortt-desc] .svg { .btn, .ui.ui.dropdown, -.flex-text-inline, -.flex-text-inline > a, -.flex-text-inline > span { +.flex-text-inline { display: inline-flex; align-items: center; gap: var(--gap-inline); diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 610e5f1344..28dd878481 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -208,7 +208,6 @@ gitea-theme-meta-info { --color-input-toggle-background: #2e353c; --color-input-border: var(--color-secondary-dark-1); --color-light: #00001728; - --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #e8f3ff28; --color-hover: #e8f3ff19; --color-hover-opaque: #21252a; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */ @@ -249,6 +248,7 @@ gitea-theme-meta-info { --color-danger: var(--color-red); --color-transparency-grid-light: #2a2a2a; --color-transparency-grid-dark: #1a1a1a; + --color-workflow-edge-hover: #616e78; accent-color: var(--color-accent); color-scheme: dark; } diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 0885c5618b..6576b88987 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -208,7 +208,6 @@ gitea-theme-meta-info { --color-input-toggle-background: #d0d7de; --color-input-border: var(--color-secondary-dark-1); --color-light: #00001706; - --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #0000171d; --color-hover: #00001708; --color-hover-opaque: #f1f3f5; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */ @@ -249,6 +248,7 @@ gitea-theme-meta-info { --color-danger: var(--color-red); --color-transparency-grid-light: #fafafa; --color-transparency-grid-dark: #e2e2e2; + --color-workflow-edge-hover: #b1b7bd; accent-color: var(--color-accent); color-scheme: light; } diff --git a/web_src/js/components/ActionRunSummaryView.vue b/web_src/js/components/ActionRunSummaryView.vue index 2d79a82288..48af966c94 100644 --- a/web_src/js/components/ActionRunSummaryView.vue +++ b/web_src/js/components/ActionRunSummaryView.vue @@ -29,35 +29,42 @@ onBeforeUnmount(() => { }); diff --git a/web_src/js/components/ActionRunView.ts b/web_src/js/components/ActionRunView.ts index 250f39e811..133b7263eb 100644 --- a/web_src/js/components/ActionRunView.ts +++ b/web_src/js/components/ActionRunView.ts @@ -89,6 +89,7 @@ export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null) export function createEmptyActionsRun(): ActionsRun { return { + repoId: 0, link: '', title: '', titleHTML: '', diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 4ced86b523..3637763b90 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -222,7 +222,11 @@ async function deleteArtifact(name: string) { max-width: 400px; position: sticky; top: 12px; - max-height: 100vh; + + /* about 12px top padding + 12px bottom padding + 37px footer height, + TODO: need to use JS to calculate the height for better scrolling experience*/ + max-height: calc(100vh - 62px); + overflow-y: auto; background: var(--color-body); z-index: 2; /* above .job-info-header */ @@ -231,12 +235,13 @@ async function deleteArtifact(name: string) { @media (max-width: 767.98px) { .action-view-left { position: static; /* can not sticky because multiple jobs would overlap into right view */ + max-height: unset; } } .left-list-header { - font-size: 12px; - color: var(--color-grey); + font-size: 13px; + color: var(--color-text-light-2); } .job-artifacts-item { @@ -299,7 +304,6 @@ async function deleteArtifact(name: string) { .job-brief-item .job-brief-item-left .job-brief-name { display: block; - width: 70%; } .job-brief-item .job-brief-item-right { @@ -320,7 +324,6 @@ async function deleteArtifact(name: string) { border: 1px solid var(--color-console-border); border-radius: var(--border-radius); background: var(--color-console-bg); - align-self: flex-start; } /* begin fomantic button overrides */ diff --git a/web_src/js/components/WorkflowGraph.vue b/web_src/js/components/WorkflowGraph.vue index c311b87d98..06ac1686e6 100644 --- a/web_src/js/components/WorkflowGraph.vue +++ b/web_src/js/components/WorkflowGraph.vue @@ -1,31 +1,31 @@ + `, - setting.StaticURLPrefix, - setting.AssetVersion, + public.AssetURI("css/swagger.css"), html.EscapeString(ctx.RenderOptions.RelativePath), html.EscapeString(util.UnsafeBytesToString(content)), - setting.StaticURLPrefix, - setting.AssetVersion, + public.AssetURI("js/swagger.js"), )) return err } diff --git a/modules/markup/render.go b/modules/markup/render.go index 5785dc5ad5..c0d44c72fc 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup/internal" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" @@ -237,10 +238,10 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, return renderIFrame(ctx, extOpts.ContentSandbox, output) } // else: this is a standalone page, fallthrough to the real rendering, and add extra JS/CSS - extraStyleHref := setting.AppSubURL + "/assets/css/external-render-iframe.css" - extraScriptSrc := setting.AppSubURL + "/assets/js/external-render-iframe.js" + extraStyleHref := public.AssetURI("css/external-render-iframe.css") + extraScriptSrc := public.AssetURI("js/external-render-iframe.js") // "`, extraScriptSrc, extraStyleHref) + extraHeadHTML = htmlutil.HTMLFormat(``, extraScriptSrc, extraStyleHref) } ctx.usedByRender = true diff --git a/modules/public/manifest.go b/modules/public/manifest.go new file mode 100644 index 0000000000..77e8959967 --- /dev/null +++ b/modules/public/manifest.go @@ -0,0 +1,156 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "io" + "path" + "sync" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +type manifestEntry struct { + File string `json:"file"` + Name string `json:"name"` + IsEntry bool `json:"isEntry"` + CSS []string `json:"css"` +} + +type manifestDataStruct struct { + paths map[string]string // unhashed path -> hashed path + names map[string]string // hashed path -> entry name + modTime int64 + checkTime time.Time +} + +var ( + manifestData atomic.Pointer[manifestDataStruct] + manifestFS = sync.OnceValue(AssetFS) +) + +const manifestPath = "assets/.vite/manifest.json" + +func parseManifest(data []byte) (map[string]string, map[string]string) { + var manifest map[string]manifestEntry + if err := json.Unmarshal(data, &manifest); err != nil { + log.Error("Failed to parse frontend manifest: %v", err) + return nil, nil + } + + paths := make(map[string]string) + names := make(map[string]string) + for _, entry := range manifest { + if !entry.IsEntry || entry.Name == "" { + continue + } + // Build unhashed key from file path: "js/index.js", "css/theme-gitea-dark.css" + dir := path.Dir(entry.File) + ext := path.Ext(entry.File) + key := dir + "/" + entry.Name + ext + paths[key] = entry.File + names[entry.File] = entry.Name + // Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css" + for _, css := range entry.CSS { + cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css) + paths[cssKey] = css + names[css] = entry.Name + } + } + return paths, names +} + +func reloadManifest(existingData *manifestDataStruct) *manifestDataStruct { + now := time.Now() + data := existingData + if data != nil && now.Sub(data.checkTime) < time.Second { + // a single request triggers multiple calls to getHashedPath + // do not check the manifest file too frequently + return data + } + + f, err := manifestFS().Open(manifestPath) + if err != nil { + log.Error("Failed to open frontend manifest: %v", err) + return data + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + log.Error("Failed to stat frontend manifest: %v", err) + return data + } + + needReload := data == nil || fi.ModTime().UnixNano() != data.modTime + if !needReload { + return data + } + manifestContent, err := io.ReadAll(f) + if err != nil { + log.Error("Failed to read frontend manifest: %v", err) + return data + } + return storeManifestFromBytes(manifestContent, fi.ModTime().UnixNano(), now) +} + +func storeManifestFromBytes(manifestContent []byte, modTime int64, checkTime time.Time) *manifestDataStruct { + paths, names := parseManifest(manifestContent) + data := &manifestDataStruct{ + paths: paths, + names: names, + modTime: modTime, + checkTime: checkTime, + } + manifestData.Store(data) + return data +} + +func getManifestData() *manifestDataStruct { + data := manifestData.Load() + + // In production the manifest is immutable (embedded in the binary). + // In dev mode, check if it changed on disk (for watch-frontend). + if data == nil || !setting.IsProd { + data = reloadManifest(data) + } + if data == nil { + data = &manifestDataStruct{} + } + return data +} + +// getHashedPath resolves an unhashed asset path (origin path) to its content-hashed path from the frontend manifest. +// Example: getHashedPath("js/index.js") returns "js/index.C6Z2MRVQ.js" +// Falls back to returning the input path unchanged if the manifest is unavailable. +func getHashedPath(originPath string) string { + data := getManifestData() + if p, ok := data.paths[originPath]; ok { + return p + } + return originPath +} + +// AssetURI returns the URI for a frontend asset. +// It may return a relative path or a full URL depending on the StaticURLPrefix setting. +// In Vite dev mode, known entry points are mapped to their source paths +// so the reverse proxy serves them from the Vite dev server. +// In production, it resolves the content-hashed path from the manifest. +func AssetURI(originPath string) string { + if src := viteDevSourceURL(originPath); src != "" { + return src + } + return setting.StaticURLPrefix + "/assets/" + getHashedPath(originPath) +} + +// AssetNameFromHashedPath returns the asset entry name for a given hashed asset path. +// Example: returns "theme-gitea-dark" for "css/theme-gitea-dark.CyAaQnn5.css". +// Returns empty string if the path is not found in the manifest. +func AssetNameFromHashedPath(hashedPath string) string { + return getManifestData().names[hashedPath] +} diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go new file mode 100644 index 0000000000..20a2232cf3 --- /dev/null +++ b/modules/public/manifest_test.go @@ -0,0 +1,91 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestViteManifest(t *testing.T) { + defer test.MockVariableValue(&setting.IsProd, true)() + + const testManifest = `{ + "web_src/js/index.ts": { + "file": "js/index.C6Z2MRVQ.js", + "name": "index", + "src": "web_src/js/index.ts", + "isEntry": true, + "css": ["css/index.B3zrQPqD.css"] + }, + "web_src/js/standalone/swagger.ts": { + "file": "js/swagger.SujiEmYM.js", + "name": "swagger", + "src": "web_src/js/standalone/swagger.ts", + "isEntry": true, + "css": ["css/swagger._-APWT_3.css"] + }, + "web_src/css/themes/theme-gitea-dark.css": { + "file": "css/theme-gitea-dark.CyAaQnn5.css", + "name": "theme-gitea-dark", + "src": "web_src/css/themes/theme-gitea-dark.css", + "isEntry": true + }, + "web_src/js/features/eventsource.sharedworker.ts": { + "file": "js/eventsource.sharedworker.Dug1twio.js", + "name": "eventsource.sharedworker", + "src": "web_src/js/features/eventsource.sharedworker.ts", + "isEntry": true + }, + "_chunk.js": { + "file": "js/chunk.abc123.js", + "name": "chunk" + } +}` + + t.Run("EmptyManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(``), 0, time.Now()) + assert.Equal(t, "/assets/js/index.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.css", AssetURI("css/theme-gitea-dark.css")) + assert.Equal(t, "", AssetNameFromHashedPath("css/no-such-file.css")) + }) + + t.Run("ParseManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(testManifest), 0, time.Now()) + paths, names := manifestData.Load().paths, manifestData.Load().names + + // JS entries + assert.Equal(t, "js/index.C6Z2MRVQ.js", paths["js/index.js"]) + assert.Equal(t, "js/swagger.SujiEmYM.js", paths["js/swagger.js"]) + assert.Equal(t, "js/eventsource.sharedworker.Dug1twio.js", paths["js/eventsource.sharedworker.js"]) + + // Associated CSS from JS entries + assert.Equal(t, "css/index.B3zrQPqD.css", paths["css/index.css"]) + assert.Equal(t, "css/swagger._-APWT_3.css", paths["css/swagger.css"]) + + // CSS-only entries + assert.Equal(t, "css/theme-gitea-dark.CyAaQnn5.css", paths["css/theme-gitea-dark.css"]) + + // Non-entry chunks should not be included + assert.Empty(t, paths["js/chunk.js"]) + + // Names: hashed path -> entry name + assert.Equal(t, "index", names["js/index.C6Z2MRVQ.js"]) + assert.Equal(t, "index", names["css/index.B3zrQPqD.css"]) + assert.Equal(t, "swagger", names["js/swagger.SujiEmYM.js"]) + assert.Equal(t, "swagger", names["css/swagger._-APWT_3.css"]) + assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"]) + assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"]) + + // Test Asset related functions + assert.Equal(t, "/assets/js/index.C6Z2MRVQ.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.CyAaQnn5.css", AssetURI("css/theme-gitea-dark.css")) + assert.Equal(t, "theme-gitea-dark", AssetNameFromHashedPath("css/theme-gitea-dark.CyAaQnn5.css")) + }) +} diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go new file mode 100644 index 0000000000..9c8da951fc --- /dev/null +++ b/modules/public/vitedev.go @@ -0,0 +1,168 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strings" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/routing" +) + +const viteDevPortFile = "public/assets/.vite/dev-port" + +var viteDevProxy atomic.Pointer[httputil.ReverseProxy] + +func getViteDevProxy() *httputil.ReverseProxy { + if proxy := viteDevProxy.Load(); proxy != nil { + return proxy + } + + portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) + data, err := os.ReadFile(portFile) + if err != nil { + return nil + } + port := strings.TrimSpace(string(data)) + if port == "" { + return nil + } + + target, err := url.Parse("http://localhost:" + port) + if err != nil { + log.Error("Failed to parse Vite dev server URL: %v", err) + return nil + } + + // there is a strange error log (from Golang's HTTP package) + // 2026/03/28 19:50:13 modules/log/misc.go:72:(*loggerToWriter).Write() [I] Unsolicited response received on idle HTTP channel starting with "HTTP/1.1 400 Bad Request\r\n\r\n"; err= + // maybe it is caused by that the Vite dev server doesn't support keep-alive connections? or different keep-alive timeouts? + transport := &http.Transport{ + IdleConnTimeout: 5 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + } + log.Info("Proxying Vite dev server requests to %s", target) + proxy := &httputil.ReverseProxy{ + Transport: transport, + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(target) + r.Out.Host = target.Host + }, + ModifyResponse: func(resp *http.Response) error { + // add a header to indicate the Vite dev server port, + // make developers know that this request is proxied to Vite dev server and which port it is + resp.Header.Add("X-Gitea-Vite-Port", port) + return nil + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + log.Error("Error proxying to Vite dev server: %v", err) + http.Error(w, "Error proxying to Vite dev server: "+err.Error(), http.StatusBadGateway) + }, + } + viteDevProxy.Store(proxy) + return proxy +} + +// ViteDevMiddleware proxies matching requests to the Vite dev server. +// It is registered as middleware in non-production mode and lazily discovers +// the Vite dev server port from the port file written by the viteDevServerPortPlugin. +// It is needed because there are container-based development, only Gitea web server's port is exposed. +func ViteDevMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + if !isViteDevRequest(req) { + next.ServeHTTP(resp, req) + return + } + proxy := getViteDevProxy() + if proxy == nil { + next.ServeHTTP(resp, req) + return + } + routing.MarkLongPolling(resp, req) + proxy.ServeHTTP(resp, req) + }) +} + +// isViteDevMode returns true if the Vite dev server port file exists. +// In production mode, the result is cached after the first check. +func isViteDevMode() bool { + if setting.IsProd { + return false + } + portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) + _, err := os.Stat(portFile) + return err == nil +} + +func viteDevSourceURL(name string) string { + if !isViteDevMode() { + return "" + } + if strings.HasPrefix(name, "css/theme-") { + // Only redirect built-in themes to Vite source; custom themes are served from custom/public/assets/css/ + themeFile := strings.TrimPrefix(name, "css/") + srcPath := filepath.Join(setting.StaticRootPath, "web_src/css/themes", themeFile) + if _, err := os.Stat(srcPath); err == nil { + return setting.AppSubURL + "/web_src/css/themes/" + themeFile + } + return "" + } + if strings.HasPrefix(name, "css/") { + return setting.AppSubURL + "/web_src/" + name + } + if name == "js/eventsource.sharedworker.js" { + return setting.AppSubURL + "/web_src/js/features/eventsource.sharedworker.ts" + } + if name == "js/iife.js" { + return setting.AppSubURL + "/web_src/js/__vite_iife.js" + } + if name == "js/index.js" { + return setting.AppSubURL + "/web_src/js/index.ts" + } + return "" +} + +// isViteDevRequest returns true if the request should be proxied to the Vite dev server. +// Ref: Vite source packages/vite/src/node/constants.ts and packages/vite/src/shared/constants.ts +func isViteDevRequest(req *http.Request) bool { + if req.Header.Get("Upgrade") == "websocket" { + wsProtocol := req.Header.Get("Sec-WebSocket-Protocol") + return wsProtocol == "vite-hmr" || wsProtocol == "vite-ping" + } + path := req.URL.Path + + // vite internal requests + if strings.HasPrefix(path, "/@vite/") /* HMR client */ || + strings.HasPrefix(path, "/@fs/") /* out-of-root file access, see vite.config.ts: fs.allow */ || + strings.HasPrefix(path, "/@id/") /* virtual modules */ { + return true + } + + // local source requests (VITE-DEV-SERVER-SECURITY: don't serve sensitive files outside the allowed paths) + if strings.HasPrefix(path, "/node_modules/") || + strings.HasPrefix(path, "/public/assets/") || + strings.HasPrefix(path, "/web_src/") { + return true + } + + // Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports: + // - {WebSite}/public/assets/... (e.g. SVG icons from "{RepoRoot}/public/assets/img/svg/") + // - {WebSite}/assets/emoji.json: it is an exception for the frontend assets, it is imported by JS code, but: + // - KEEP IN MIND: all static frontend assets are served from "{AssetFS}/assets" to "{WebSite}/assets" by Gitea Web Server + // - "{AssetFS}" is a layered filesystem from "{RepoRoot}/public" or embedded assets, and user's custom files in "{CustomPath}/public" + // - "{RepoRoot}/assets/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets + // - BAD DESIGN: indeed it is a "conflicted and polluted name" sample + if path == "/assets/emoji.json" { + return true + } + return false +} diff --git a/modules/setting/server.go b/modules/setting/server.go index f0fbbce970..1085e052a3 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -72,9 +72,6 @@ var ( // It maps to ini:"LOCAL_ROOT_URL" in [server] LocalURL string - // AssetVersion holds an opaque value that is used for cache-busting assets - AssetVersion string - // appTempPathInternal is the temporary path for the app, it is only an internal variable // DO NOT use it directly, always use AppDataTempDir appTempPathInternal string @@ -317,8 +314,6 @@ func loadServerFrom(rootCfg ConfigProvider) { } AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix) - AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed) - manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index d2d4d364df..3a5eb5904f 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -6,15 +6,18 @@ package templates import ( "fmt" + "html" "html/template" "net/url" "strconv" "strings" + "sync" "time" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates/eval" @@ -68,6 +71,8 @@ func NewFuncMap() template.FuncMap { return strconv.FormatInt(time.Since(startTime).Nanoseconds()/1e6, 10) + "ms" }, + "AssetURI": public.AssetURI, + "ScriptImport": scriptImport, // ----------------------------------------------------------------- // setting "AppName": func() string { @@ -92,9 +97,6 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, - "AssetVersion": func() string { - return setting.AssetVersion - }, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, @@ -303,3 +305,30 @@ func QueryBuild(a ...any) template.URL { } return template.URL(s) } + +var globalVars = sync.OnceValue(func() (ret struct { + scriptImportRemainingPart string +}, +) { + // add onerror handler to alert users when the script fails to load: + // * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config + // * for developers: help them to remember to run "make watch-frontend" to build frontend assets + // the message will be directly put in the onerror JS code's string + onScriptErrorPrompt := `Please make sure the asset files can be accessed.` + if !setting.IsProd { + onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.` + } + onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt) + ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `">` + return ret +}) + +func scriptImport(path string, typ ...string) template.HTML { + if len(typ) > 0 { + if typ[0] == "module" { + return template.HTML(` - +{{ScriptImport "js/iife.js"}} diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index b2fc033558..15fa7ad730 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - - + + diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl index a1b3b86e5c..868136e194 100644 --- a/templates/devtest/devtest-footer.tmpl +++ b/templates/devtest/devtest-footer.tmpl @@ -1,3 +1,3 @@ {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - + {{template "base/footer" ctx.RootData}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 0775dccc2d..a7aebcb7dc 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,3 +1,8 @@ {{template "base/head" ctx.RootData}} - + + {{template "base/alert" .}} diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index 424f590f84..c230fadb16 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,5 +1,5 @@ {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics. -* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl +* base template functions: AppName, AssetUrlPrefix, AssetURI, AppSubUrl * ctx.Locale * .Flash * .ErrorMsg diff --git a/templates/swagger/ui.tmpl b/templates/swagger/ui.tmpl index 4ff3472807..d53a611176 100644 --- a/templates/swagger/ui.tmpl +++ b/templates/swagger/ui.tmpl @@ -2,13 +2,13 @@ Gitea API - + {{/* TODO: add Help & Glossary to help users understand the API, and explain some concepts like "Owner" */}} {{svg "octicon-reply"}}{{ctx.Locale.Tr "return_to_gitea"}}
    - + diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 691ffcc62b..3d9d7b3969 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -107,7 +108,7 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
    <script></script>
    `, respSub.Body.String()) + assert.Equal(t, `
    <script></script>
    `, respSub.Body.String()) }) }) @@ -130,7 +131,7 @@ func TestExternalMarkupRenderer(t *testing.T) { t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") respSub := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, ``, respSub.Body.String()) assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) }) }) diff --git a/tsconfig.json b/tsconfig.json index 9b978cf54e..851bf13dc9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -45,7 +45,7 @@ "verbatimModuleSyntax": true, "types": [ "node", - "webpack/module", + "vite/client", "vitest/globals", "./web_src/js/globals.d.ts", "./types.d.ts", diff --git a/types.d.ts b/types.d.ts index 59d6ecf149..234bd267fe 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,8 +1,3 @@ -declare module '@techknowlogick/license-checker-webpack-plugin' { - const plugin: any; - export = plugin; -} - declare module 'eslint-plugin-no-use-extend-native' { import type {Eslint} from 'eslint'; const plugin: Eslint.Plugin; diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000..d2c7abac05 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,332 @@ +import {build, defineConfig} from 'vite'; +import vuePlugin from '@vitejs/plugin-vue'; +import {stringPlugin} from 'vite-string-plugin'; +import {readFileSync, writeFileSync, unlinkSync, globSync} from 'node:fs'; +import {join, parse} from 'node:path'; +import {env} from 'node:process'; +import tailwindcss from 'tailwindcss'; +import tailwindConfig from './tailwind.config.ts'; +import wrapAnsi from 'wrap-ansi'; +import licensePlugin from 'rollup-plugin-license'; +import type {InlineConfig, Plugin, Rolldown} from 'vite'; + +const isProduction = env.NODE_ENV !== 'development'; + +// ENABLE_SOURCEMAP accepts the following values: +// true - all sourcemaps enabled, the default in development +// reduced - sourcemaps only for index.js, the default in production +// false - all sourcemaps disabled +let enableSourcemap: string; +if ('ENABLE_SOURCEMAP' in env) { + enableSourcemap = ['true', 'false'].includes(env.ENABLE_SOURCEMAP!) ? env.ENABLE_SOURCEMAP! : 'reduced'; +} else { + enableSourcemap = isProduction ? 'reduced' : 'true'; +} +const outDir = join(import.meta.dirname, 'public/assets'); + +const themes: Record = {}; +for (const path of globSync('web_src/css/themes/*.css', {cwd: import.meta.dirname})) { + themes[parse(path).name] = join(import.meta.dirname, path); +} + +const webComponents = new Set([ + // our own, in web_src/js/webcomponents + 'overflow-menu', + 'origin-url', + 'relative-time', + // from dependencies + 'markdown-toolbar', + 'text-expander', +]); + +function formatLicenseText(licenseText: string) { + return wrapAnsi(licenseText || '', 80).trim(); +} + +const commonRolldownOptions: Rolldown.RolldownOptions = { + checks: { + eval: false, // htmx needs eval + pluginTimings: false, + }, +}; + +function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { + const {rolldownOptions, ...otherBuild} = build || {}; + return { + base: './', // make all asset URLs relative, so it works in subdirectory deployments + configFile: false, + root: import.meta.dirname, + publicDir: false, + build: { + outDir, + emptyOutDir: false, + sourcemap: enableSourcemap !== 'false', + target: 'es2020', + minify: isProduction ? 'oxc' : false, + cssMinify: isProduction ? 'esbuild' : false, + chunkSizeWarningLimit: Infinity, + assetsInlineLimit: 32768, + reportCompressedSize: false, + rolldownOptions: { + ...commonRolldownOptions, + ...rolldownOptions, + }, + ...otherBuild, + }, + ...other, + }; +} + +const iifeEntry = join(import.meta.dirname, 'web_src/js/iife.ts'); + +function iifeBuildOpts({entryFileNames, write}: {entryFileNames: string, write?: boolean}) { + return commonViteOpts({ + build: { + lib: {entry: iifeEntry, formats: ['iife'], name: 'iife'}, + rolldownOptions: {output: {entryFileNames}}, + ...(write === false && {write: false}), + }, + plugins: [stringPlugin()], + }); +} + +// Build iife.js as a blocking IIFE bundle. In dev mode, serves it from memory +// and rebuilds on file changes. In prod mode, writes to disk during closeBundle. +function iifePlugin(): Plugin { + let iifeCode = ''; + let iifeMap = ''; + const iifeModules = new Set(); + let isBuilding = false; + return { + name: 'iife', + async configureServer(server) { + const buildAndCache = async () => { + const result = await build(iifeBuildOpts({entryFileNames: 'js/iife.js', write: false})); + const output = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput; + const chunk = output.output[0]; + iifeCode = chunk.code.replace(/\/\/# sourceMappingURL=.*/, '//# sourceMappingURL=__vite_iife.js.map'); + const mapAsset = output.output.find((o) => o.fileName.endsWith('.map')); + iifeMap = mapAsset && 'source' in mapAsset ? String(mapAsset.source) : ''; + iifeModules.clear(); + for (const id of Object.keys(chunk.modules)) iifeModules.add(id); + }; + await buildAndCache(); + + let needsRebuild = false; + server.watcher.on('change', async (path) => { + if (!iifeModules.has(path)) return; + needsRebuild = true; + if (isBuilding) return; + isBuilding = true; + try { + do { + needsRebuild = false; + await buildAndCache(); + } while (needsRebuild); + server.ws.send({type: 'full-reload'}); + } finally { + isBuilding = false; + } + }); + + server.middlewares.use((req, res, next) => { + // "__vite_iife" is a virtual file in memory, serve it directly + const pathname = req.url!.split('?')[0]; + if (pathname === '/web_src/js/__vite_iife.js') { + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Cache-Control', 'no-store'); + res.end(iifeCode); + } else if (pathname === '/web_src/js/__vite_iife.js.map') { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'no-store'); + res.end(iifeMap); + } else { + next(); + } + }); + }, + async closeBundle() { + for (const file of globSync('js/iife.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); + const result = await build(iifeBuildOpts({entryFileNames: 'js/iife.[hash:8].js'})); + const buildOutput = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput; + const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/iife.')); + if (!entry) throw new Error('IIFE build produced no output'); + const manifestPath = join(outDir, '.vite', 'manifest.json'); + writeFileSync(manifestPath, JSON.stringify({ + ...JSON.parse(readFileSync(manifestPath, 'utf8')), + 'web_src/js/iife.ts': {file: entry.fileName, name: 'iife', isEntry: true}, + }, null, 2)); + }, + }; +} + +// In reduced sourcemap mode, only keep sourcemaps for main files +function reducedSourcemapPlugin(): Plugin { + return { + name: 'reduced-sourcemap', + apply: 'build', + closeBundle() { + if (enableSourcemap !== 'reduced') return; + for (const file of globSync('{js,css}/*.map', {cwd: outDir})) { + if (!file.startsWith('js/index.') && !file.startsWith('js/iife.')) unlinkSync(join(outDir, file)); + } + }, + }; +} + +// Filter out legacy font formats from CSS, keeping only woff2 +function filterCssUrlPlugin(): Plugin { + return { + name: 'filter-css-url', + enforce: 'pre', + transform(code, id) { + if (!id.endsWith('.css') || !id.includes('katex')) return null; + return code.replace(/,\s*url\([^)]*\.(?:woff|ttf)\)\s*format\("[^"]*"\)/gi, ''); + }, + }; +} + +const viteDevServerPort = Number(env.FRONTEND_DEV_SERVER_PORT) || 3001; +const viteDevPortFilePath = join(outDir, '.vite', 'dev-port'); + +// Write the Vite dev server's actual port to a file so the Go server can discover it for proxying. +function viteDevServerPortPlugin(): Plugin { + return { + name: 'vite-dev-server-port', + apply: 'serve', + configureServer(server) { + server.httpServer!.once('listening', () => { + const addr = server.httpServer!.address(); + if (typeof addr === 'object' && addr) { + writeFileSync(viteDevPortFilePath, String(addr.port)); + } + }); + }, + }; +} + +export default defineConfig(commonViteOpts({ + appType: 'custom', // Go serves all HTML, disable Vite's HTML handling + clearScreen: false, + server: { + port: viteDevServerPort, + open: false, + host: '0.0.0.0', + strictPort: false, + fs: { + // VITE-DEV-SERVER-SECURITY: the dev server will be exposed to public by Gitea's web server, so we need to strictly limit the access + // Otherwise `/@fs/*` will be able to access any file (including app.ini which contains INTERNAL_TOKEN) + strict: true, + allow: [ + 'assets', + 'node_modules', + 'public', + 'web_src', + // do not add any other directories here, unless you are absolutely sure it's safe to expose them to the public + ], + }, + headers: { + 'Cache-Control': 'no-store', // prevent browser disk cache + }, + warmup: { + clientFiles: [ + // warmup the important entry points + 'web_src/js/index.ts', + 'web_src/css/index.css', + 'web_src/css/themes/*.css', + ], + }, + }, + build: { + modulePreload: false, + manifest: true, + rolldownOptions: { + input: { + index: join(import.meta.dirname, 'web_src/js/index.ts'), + swagger: join(import.meta.dirname, 'web_src/js/standalone/swagger.ts'), + 'external-render-iframe': join(import.meta.dirname, 'web_src/js/standalone/external-render-iframe.ts'), + 'eventsource.sharedworker': join(import.meta.dirname, 'web_src/js/features/eventsource.sharedworker.ts'), + ...(!isProduction && { + devtest: join(import.meta.dirname, 'web_src/js/standalone/devtest.ts'), + }), + ...themes, + }, + output: { + entryFileNames: 'js/[name].[hash:8].js', + chunkFileNames: 'js/[name].[hash:8].js', + assetFileNames: ({names}) => { + const name = names[0]; + if (name.endsWith('.css')) return 'css/[name].[hash:8].css'; + if (/\.(ttf|woff2?)$/.test(name)) return 'fonts/[name].[hash:8].[ext]'; + return '[name].[hash:8].[ext]'; + }, + }, + }, + }, + worker: { + rolldownOptions: { + ...commonRolldownOptions, + output: { + entryFileNames: 'js/[name].[hash:8].js', + }, + }, + }, + css: { + transformer: 'postcss', + postcss: { + plugins: [ + tailwindcss(tailwindConfig), + ], + }, + }, + define: { + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, + }, + plugins: [ + iifePlugin(), + viteDevServerPortPlugin(), + reducedSourcemapPlugin(), + filterCssUrlPlugin(), + stringPlugin(), + vuePlugin({ + template: { + compilerOptions: { + isCustomElement: (tag) => webComponents.has(tag), + }, + }, + }), + isProduction ? licensePlugin({ + thirdParty: { + output: { + file: join(import.meta.dirname, 'public/assets/licenses.txt'), + template(deps) { + const line = '-'.repeat(80); + const goJson = readFileSync(join(import.meta.dirname, 'assets/go-licenses.json'), 'utf8'); + const goModules = JSON.parse(goJson).map(({name, licenseText}: {name: string, licenseText: string}) => { + return {name, body: formatLicenseText(licenseText)}; + }); + const jsModules = deps.map((dep) => { + return {name: dep.name, version: dep.version, body: formatLicenseText(dep.licenseText ?? '')}; + }); + const modules = [...goModules, ...jsModules].sort((a, b) => a.name.localeCompare(b.name)); + return modules.map(({name, version, body}: {name: string, version?: string, body: string}) => { + const title = version ? `${name}@${version}` : name; + return `${line}\n${title}\n${line}\n${body}`; + }).join('\n'); + }, + }, + allow(dependency) { + if (dependency.name === 'khroma') return true; // MIT: https://github.com/fabiospampinato/khroma/pull/33 + return /(Apache-2\.0|0BSD|BSD-2-Clause|BSD-3-Clause|MIT|ISC|CPAL-1\.0|Unlicense|EPL-1\.0|EPL-2\.0)/.test(dependency.license ?? ''); + }, + }, + }) : { + name: 'dev-licenses-stub', + closeBundle() { + writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development'); + }, + }, + ], +})); diff --git a/web_src/css/base.css b/web_src/css/base.css index 60317887ba..b660e19ac4 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -538,6 +538,58 @@ strong.attention-caution, svg.attention-caution { overflow-menu { border-bottom: 1px solid var(--color-secondary) !important; display: flex; + position: relative; +} + +overflow-menu .overflow-menu-popup { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 100; + background-color: var(--color-menu); + color: var(--color-text); + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + box-shadow: 0 6px 18px var(--color-shadow); + padding: 4px 0; +} + +overflow-menu .overflow-menu-popup::before, +overflow-menu .overflow-menu-popup::after { + content: ""; + position: absolute; + right: 10px; + border: 8px solid transparent; +} + +overflow-menu .overflow-menu-popup::before { + bottom: 100%; + border-bottom-color: var(--color-secondary); +} + +overflow-menu .overflow-menu-popup::after { + bottom: calc(100% - 1px); + border-bottom-color: var(--color-menu); +} + +overflow-menu .overflow-menu-popup > .item { + display: flex; + align-items: center; + padding: 9px 18px !important; + color: var(--color-text) !important; + background: transparent !important; + text-decoration: none; + gap: 10px; + width: 100%; +} + +overflow-menu .overflow-menu-popup > .item:hover, +overflow-menu .overflow-menu-popup > .item:focus { + background: var(--color-hover) !important; +} + +overflow-menu .overflow-menu-popup > .item.active { + background: var(--color-active) !important; } overflow-menu .overflow-menu-items { diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index ca38ac874e..f88f490063 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -1,82 +1,12 @@ // DO NOT IMPORT window.config HERE! // to make sure the error handler always works, we should never import `window.config`, because // some user's custom template breaks it. -import type {Intent} from './types.ts'; -import {html} from './utils/html.ts'; +import {showGlobalErrorMessage, processWindowErrorEvent} from './modules/errors.ts'; -// This sets up the URL prefix used in webpack's chunk loading. -// This file must be imported before any lazy-loading is being attempted. -window.__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`; - -export function shouldIgnoreError(err: Error) { - const ignorePatterns: Array = [ - // https://github.com/go-gitea/gitea/issues/30861 - // https://github.com/microsoft/monaco-editor/issues/4496 - // https://github.com/microsoft/monaco-editor/issues/4679 - /\/assets\/js\/.*monaco/, - ]; - for (const pattern of ignorePatterns) { - if (pattern.test(err.stack ?? '')) return true; - } - return false; -} - -export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { - const msgContainer = document.querySelector('.page-content') ?? document.body; - if (!msgContainer) { - alert(`${msgType}: ${msg}`); - return; - } - const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages - let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); - if (!msgDiv) { - const el = document.createElement('div'); - el.innerHTML = html`
    `; - msgDiv = el.childNodes[0] as HTMLDivElement; - } - // merge duplicated messages into "the message (count)" format - const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; - msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); - msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); - msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); - msgContainer.prepend(msgDiv); -} - -function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) { - const err = error ?? reason; - const assetBaseUrl = String(new URL(window.__webpack_public_path__, window.location.origin)); - const {runModeIsProd} = window.config ?? {}; - - // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a - // non-critical event from the browser. We log them but don't show them to users. Examples: - // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors - // - https://github.com/mozilla-mobile/firefox-ios/issues/10817 - // - https://github.com/go-gitea/gitea/issues/20240 - if (!err) { - if (message) console.error(new Error(message)); - if (runModeIsProd) return; - } - - if (err instanceof Error) { - // If the error stack trace does not include the base URL of our script assets, it likely came - // from a browser extension or inline script. Do not show such errors in production. - if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return; - // Ignore some known errors that are unable to fix - if (shouldIgnoreError(err)) return; - } - - let msg = err?.message ?? message; - if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`; - const dot = msg.endsWith('.') ? '' : '.'; - const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type; - showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`); -} - -function initGlobalErrorHandler() { - if (window._globalHandlerErrors?._inited) { - showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`); - return; - } +// A module should not be imported twice, otherwise there will be bugs when a module has its internal states. +// A real example is "generateElemId" in "utils/dom.ts", if it is imported twice in different module scopes, +// It will generate duplicate IDs (ps: don't try to use "random" to fix, it is just a real example to show the importance of "do not import a module twice") +if (!window._globalHandlerErrors?._inited) { if (!window.config) { showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); } @@ -90,5 +20,3 @@ function initGlobalErrorHandler() { // events directly window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)} as any; } - -initGlobalErrorHandler(); diff --git a/web_src/js/features/captcha.ts b/web_src/js/features/captcha.ts index 01b5053026..08513fe6ba 100644 --- a/web_src/js/features/captcha.ts +++ b/web_src/js/features/captcha.ts @@ -34,7 +34,7 @@ export async function initCaptcha() { break; } case 'm-captcha': { - const mCaptcha = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue'); + const mCaptcha = await import('@mcaptcha/vanilla-glue'); // FIXME: the mCaptcha code is not right, it's a miracle that the wrong code could run // * the "vanilla-glue" has some problems with es6 module. diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts index 6d30d81685..1abd960366 100644 --- a/web_src/js/features/citation.ts +++ b/web_src/js/features/citation.ts @@ -6,10 +6,10 @@ const {pageData} = window.config; async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { const [{Cite, plugins}] = await Promise.all([ - import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), - import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'), - import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'), - import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'), + import('@citation-js/core'), + import('@citation-js/plugin-software-formats'), + import('@citation-js/plugin-bibtex'), + import('@citation-js/plugin-csl'), ]); const citationFileContent = pageData.citationFileContent!; const config = plugins.config.get('@bibtex'); diff --git a/web_src/js/features/code-frequency.ts b/web_src/js/features/code-frequency.ts index da7cd6b2c0..475379ac14 100644 --- a/web_src/js/features/code-frequency.ts +++ b/web_src/js/features/code-frequency.ts @@ -4,7 +4,7 @@ export async function initRepoCodeFrequency() { const el = document.querySelector('#repo-code-frequency-chart'); if (!el) return; - const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue'); + const {default: RepoCodeFrequency} = await import('../components/RepoCodeFrequency.vue'); try { const View = createApp(RepoCodeFrequency, { locale: { diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index dc3f2fad81..58acf1494d 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -129,7 +129,7 @@ function updateTheme(monaco: Monaco): void { type CreateMonacoOpts = MonacoOpts & {language?: string}; export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts): Promise<{monaco: Monaco, editor: IStandaloneCodeEditor}> { - const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); + const monaco = await import('../modules/monaco.ts'); initLanguages(monaco); let {language, ...other} = opts; diff --git a/web_src/js/features/colorpicker.ts b/web_src/js/features/colorpicker.ts index face4ef228..6a14774bfc 100644 --- a/web_src/js/features/colorpicker.ts +++ b/web_src/js/features/colorpicker.ts @@ -6,8 +6,8 @@ export async function initColorPickers() { registerGlobalInitFunc('initColorPicker', async (el) => { if (!imported) { await Promise.all([ - import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'), - import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'), + import('vanilla-colorful/hex-color-picker.js'), + import('../../css/features/colorpicker.css'), ]); imported = true; } diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts index 36af087089..fd37e307f7 100644 --- a/web_src/js/features/common-page.ts +++ b/web_src/js/features/common-page.ts @@ -1,5 +1,5 @@ import {GET, POST} from '../modules/fetch.ts'; -import {showGlobalErrorMessage} from '../bootstrap.ts'; +import {showGlobalErrorMessage} from '../modules/errors.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {addDelegatedEventListener, queryElems} from '../utils/dom.ts'; import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 5b470ea03d..468f3fc5ca 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -319,8 +319,8 @@ export class ComboMarkdownEditor { async switchToEasyMDE() { if (this.easyMDE) return; const [{default: EasyMDE}] = await Promise.all([ - import(/* webpackChunkName: "easymde" */'easymde'), - import(/* webpackChunkName: "easymde" */'../../../css/easymde.css'), + import('easymde'), + import('../../../css/easymde.css'), ]); const easyMDEOpt: EasyMDE.Options = { autoDownloadFontAwesome: false, diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index 9fd48697fa..a36689bfc2 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -7,7 +7,7 @@ type CropperOpts = { }; async function initCompCropper({container, fileInput, imageSource}: CropperOpts) { - const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs'); + const {default: Cropper} = await import('cropperjs'); let currentFileName = ''; let currentFileLastModified = 0; const cropper = new Cropper(imageSource, { diff --git a/web_src/js/features/contributors.ts b/web_src/js/features/contributors.ts index 95fc81f5b3..d28d18eacc 100644 --- a/web_src/js/features/contributors.ts +++ b/web_src/js/features/contributors.ts @@ -4,7 +4,7 @@ export async function initRepoContributors() { const el = document.querySelector('#repo-contributors-chart'); if (!el) return; - const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue'); + const {default: RepoContributors} = await import('../components/RepoContributors.vue'); try { const View = createApp(RepoContributors, { repoLink: el.getAttribute('data-repo-link'), diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts index fedcff2162..55c0e3c7a5 100644 --- a/web_src/js/features/dropzone.ts +++ b/web_src/js/features/dropzone.ts @@ -19,8 +19,8 @@ export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; async function createDropzone(el: HTMLElement, opts: DropzoneOptions) { const [{default: Dropzone}] = await Promise.all([ - import(/* webpackChunkName: "dropzone" */'dropzone'), - import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), + import('dropzone'), + import('dropzone/dist/dropzone.css'), ]); return new Dropzone(el, opts); } diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts index 95004096d8..341e014bfc 100644 --- a/web_src/js/features/heatmap.ts +++ b/web_src/js/features/heatmap.ts @@ -45,7 +45,7 @@ export async function initHeatmap() { noDataText: el.getAttribute('data-locale-no-contributions'), }; - const {default: ActivityHeatmap} = await import(/* webpackChunkName: "ActivityHeatmap" */ '../components/ActivityHeatmap.vue'); + const {default: ActivityHeatmap} = await import('../components/ActivityHeatmap.vue'); const View = createApp(ActivityHeatmap, {values, locale}); View.mount(el); el.classList.remove('is-loading'); diff --git a/web_src/js/features/recent-commits.ts b/web_src/js/features/recent-commits.ts index b7f7c49987..6ad53a238c 100644 --- a/web_src/js/features/recent-commits.ts +++ b/web_src/js/features/recent-commits.ts @@ -4,7 +4,7 @@ export async function initRepoRecentCommits() { const el = document.querySelector('#repo-recent-commits-chart'); if (!el) return; - const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue'); + const {default: RepoRecentCommits} = await import('../components/RepoRecentCommits.vue'); try { const View = createApp(RepoRecentCommits, { locale: { diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 8d306b2bab..962f8b84c1 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -69,7 +69,7 @@ export function filterRepoFilesWeighted(files: Array, filter: string) { export function initRepoFileSearch() { registerGlobalInitFunc('initRepoFileSearch', async (el) => { - const {default: RepoFileSearch} = await import(/* webpackChunkName: "RepoFileSearch" */ '../components/RepoFileSearch.vue'); + const {default: RepoFileSearch} = await import('../components/RepoFileSearch.vue'); createApp(RepoFileSearch, { repoLink: el.getAttribute('data-repo-link'), currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'), diff --git a/web_src/js/features/repo-issue-pull.ts b/web_src/js/features/repo-issue-pull.ts index 093f484b42..58dbf1790e 100644 --- a/web_src/js/features/repo-issue-pull.ts +++ b/web_src/js/features/repo-issue-pull.ts @@ -66,7 +66,7 @@ async function initRepoPullRequestMergeForm(box: HTMLElement) { const el = box.querySelector('#pull-request-merge-form'); if (!el) return; - const {default: PullRequestMergeForm} = await import(/* webpackChunkName: "PullRequestMergeForm" */ '../components/PullRequestMergeForm.vue'); + const {default: PullRequestMergeForm} = await import('../components/PullRequestMergeForm.vue'); const view = createApp(PullRequestMergeForm); view.mount(el); } diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts index 1a011c33a1..462a925ab6 100644 --- a/web_src/js/features/tribute.ts +++ b/web_src/js/features/tribute.ts @@ -5,7 +5,7 @@ import type {TributeCollection} from 'tributejs'; import type {Mention} from '../types.ts'; export async function attachTribute(element: HTMLElement) { - const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); + const {default: Tribute} = await import('tributejs'); const mentionsUrl = element.closest('[data-mentions-url]')?.getAttribute('data-mentions-url'); const emojiCollection: TributeCollection = { // emojis diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index f6e0a109b0..2a6f86b65e 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -22,8 +22,8 @@ interface Window { config: { appUrl: string, appSubUrl: string, - assetVersionEncoded: string, assetUrlPrefix: string, + sharedWorkerUri: string, runModeIsProd: boolean, customEmojis: Record, pageData: Record & { @@ -64,6 +64,10 @@ interface Window { codeEditors: any[], // export editor for customization localUserSettings: typeof import('./modules/user-settings.ts').localUserSettings, + MonacoEnvironment?: { + getWorker: (workerId: string, label: string) => Worker, + }, + // various captcha plugins grecaptcha: any, turnstile: any, @@ -71,3 +75,8 @@ interface Window { // do not add more properties here unless it is a must } + +declare module '*?worker' { + const workerConstructor: new () => Worker; + export default workerConstructor; +} diff --git a/web_src/js/globals.ts b/web_src/js/globals.ts index 955515d250..9cd66d8322 100644 --- a/web_src/js/globals.ts +++ b/web_src/js/globals.ts @@ -1,2 +1,16 @@ -import jquery from 'jquery'; -window.$ = window.jQuery = jquery; // only for Fomantic UI +import jquery from 'jquery'; // eslint-disable-line no-restricted-imports +import htmx from 'htmx.org'; // eslint-disable-line no-restricted-imports +import 'idiomorph/htmx'; // eslint-disable-line no-restricted-imports + +// Some users still use inline scripts and expect jQuery to be available globally. +// To avoid breaking existing users and custom plugins, import jQuery globally without ES module. +window.$ = window.jQuery = jquery; + +// There is a bug in htmx, it incorrectly checks "readyState === 'complete'" when the DOM tree is ready and won't trigger DOMContentLoaded +// The bug makes htmx impossible to be loaded from an ES module: importing the htmx in onDomReady will make htmx skip its initialization. +// ref: https://github.com/bigskysoftware/htmx/pull/3365 +window.htmx = htmx; + +// https://htmx.org/reference/#config +htmx.config.requestClass = 'is-loading'; +htmx.config.scrollIntoViewOnBoost = false; diff --git a/web_src/js/htmx.ts b/web_src/js/htmx.ts deleted file mode 100644 index acc3df1d81..0000000000 --- a/web_src/js/htmx.ts +++ /dev/null @@ -1,26 +0,0 @@ -import htmx from 'htmx.org'; -import 'idiomorph/htmx'; -import type {HtmxResponseInfo} from 'htmx.org'; -import {showErrorToast} from './modules/toast.ts'; - -type HtmxEvent = Event & {detail: HtmxResponseInfo}; - -export function initHtmx() { - window.htmx = htmx; - - // https://htmx.org/reference/#config - htmx.config.requestClass = 'is-loading'; - htmx.config.scrollIntoViewOnBoost = false; - - // https://htmx.org/events/#htmx:sendError - document.body.addEventListener('htmx:sendError', (event: Partial) => { - // TODO: add translations - showErrorToast(`Network error when calling ${event.detail!.requestConfig.path}`); - }); - - // https://htmx.org/events/#htmx:responseError - document.body.addEventListener('htmx:responseError', (event: Partial) => { - // TODO: add translations - showErrorToast(`Error ${event.detail!.xhr.status} when calling ${event.detail!.requestConfig.path}`); - }); -} diff --git a/web_src/js/iife.ts b/web_src/js/iife.ts new file mode 100644 index 0000000000..218519c59a --- /dev/null +++ b/web_src/js/iife.ts @@ -0,0 +1,11 @@ +// This file is the entry point for the code which should block the page rendering, it is compiled by our "iife" vite plugin + +// bootstrap module must be the first one to be imported, it handles global errors +import './bootstrap.ts'; + +// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) +// so load globals (including jQuery) as early as possible +import './globals.ts'; + +import './webcomponents/index.ts'; +import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts deleted file mode 100644 index 19a61b0e40..0000000000 --- a/web_src/js/index-domready.ts +++ /dev/null @@ -1,175 +0,0 @@ -import '../fomantic/build/fomantic.js'; - -import {initHtmx} from './htmx.ts'; -import {initDashboardRepoList} from './features/dashboard.ts'; -import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; -import {initRepoGraphGit} from './features/repo-graph.ts'; -import {initHeatmap} from './features/heatmap.ts'; -import {initImageDiff} from './features/imagediff.ts'; -import {initRepoMigration} from './features/repo-migration.ts'; -import {initRepoProject} from './features/repo-projects.ts'; -import {initTableSort} from './features/tablesort.ts'; -import {initAdminUserListSearchForm} from './features/admin/users.ts'; -import {initAdminConfigs} from './features/admin/config.ts'; -import {initMarkupAnchors} from './markup/anchors.ts'; -import {initNotificationCount} from './features/notification.ts'; -import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; -import {initStopwatch} from './features/stopwatch.ts'; -import {initRepoFileSearch} from './features/repo-findfile.ts'; -import {initMarkupContent} from './markup/content.ts'; -import {initRepoFileView} from './features/file-view.ts'; -import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; -import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; -import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; -import {initRepoTopicBar} from './features/repo-home.ts'; -import {initAdminCommon} from './features/admin/common.ts'; -import {initRepoCodeView} from './features/repo-code.ts'; -import {initSshKeyFormParser} from './features/sshkey-helper.ts'; -import {initUserSettings} from './features/user-settings.ts'; -import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; -import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; -import {initRepoDiffView} from './features/repo-diff.ts'; -import {initOrgTeam} from './features/org-team.ts'; -import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts'; -import {initRepoReleaseNew} from './features/repo-release.ts'; -import {initRepoEditor} from './features/repo-editor.ts'; -import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts'; -import {initInstall} from './features/install.ts'; -import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts'; -import {initRepoBranchButton} from './features/repo-branch.ts'; -import {initCommonOrganization} from './features/common-organization.ts'; -import {initRepoWikiForm} from './features/repo-wiki.ts'; -import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts'; -import {initCopyContent} from './features/copycontent.ts'; -import {initCaptcha} from './features/captcha.ts'; -import {initRepositoryActionView} from './features/repo-actions.ts'; -import {initGlobalTooltips} from './modules/tippy.ts'; -import {initGiteaFomantic} from './modules/fomantic.ts'; -import {initSubmitEventPolyfill} from './utils/dom.ts'; -import {initRepoIssueList} from './features/repo-issue-list.ts'; -import {initCommonIssueListQuickGoto} from './features/common-issue-list.ts'; -import {initRepoContributors} from './features/contributors.ts'; -import {initRepoCodeFrequency} from './features/code-frequency.ts'; -import {initRepoRecentCommits} from './features/recent-commits.ts'; -import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts'; -import {initGlobalSelectorObserver} from './modules/observer.ts'; -import {initRepositorySearch} from './features/repo-search.ts'; -import {initColorPickers} from './features/colorpicker.ts'; -import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; -import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; -import {initGlobalFetchAction} from './features/common-fetch-action.ts'; -import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts'; -import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts'; -import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; -import {callInitFunctions} from './modules/init.ts'; -import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; -import {initActionsPermissionsForm} from './features/common-actions-permissions.ts'; -import {initGlobalShortcut} from './modules/shortcut.ts'; - -const initStartTime = performance.now(); -const initPerformanceTracer = callInitFunctions([ - initHtmx, - initSubmitEventPolyfill, - initGiteaFomantic, - - initGlobalComponent, - initGlobalDropdown, - initGlobalFetchAction, - initGlobalTooltips, - initGlobalButtonClickOnEnter, - initGlobalButtons, - initGlobalCopyToClipboardListener, - initGlobalEnterQuickSubmit, - initGlobalFormDirtyLeaveConfirm, - initGlobalComboMarkdownEditor, - initGlobalDeleteButton, - initGlobalInput, - initGlobalShortcut, - - initCommonOrganization, - initCommonIssueListQuickGoto, - - initCompSearchUserBox, - initCompWebHookEditor, - - initInstall, - - initCommmPageComponents, - - initHeatmap, - initImageDiff, - initMarkupAnchors, - initMarkupContent, - initSshKeyFormParser, - initStopwatch, - initTableSort, - initRepoFileSearch, - initCopyContent, - - initAdminCommon, - initAdminUserListSearchForm, - initAdminConfigs, - initAdminSelfCheck, - - initDashboardRepoList, - - initNotificationCount, - - initOrgTeam, - - initRepoActivityTopAuthorsChart, - initRepoArchiveLinks, - initRepoBranchButton, - initRepoCodeView, - initBranchSelectorTabs, - initRepoEllipsisButton, - initRepoDiffCommitBranchesAndTags, - initRepoEditor, - initRepoGraphGit, - initRepoIssueContentHistory, - initRepoIssueList, - initRepoIssueFilterItemLabel, - initRepoIssueSidebarDependency, - initRepoMigration, - initRepoMigrationStatusChecker, - initRepoProject, - initRepoPullRequestAllowMaintainerEdit, - initRepoPullRequestReview, - initRepoReleaseNew, - initRepoTopicBar, - initRepoViewFileTree, - initRepoWikiForm, - initRepository, - initRepositoryActionView, - initRepositorySearch, - initRepoContributors, - initRepoCodeFrequency, - initRepoRecentCommits, - - initCommitStatuses, - initCaptcha, - - initUserCheckAppUrl, - initUserAuthOauth2, - initUserAuthWebAuthn, - initUserAuthWebAuthnRegister, - initUserSettings, - initRepoDiffView, - initColorPickers, - - initOAuth2SettingsDisableCheckbox, - - initRepoFileView, - initActionsPermissionsForm, -]); - -// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions. -initGlobalSelectorObserver(initPerformanceTracer); -if (initPerformanceTracer) initPerformanceTracer.printResults(); - -const initDur = performance.now() - initStartTime; -if (initDur > 500) { - console.error(`slow init functions took ${initDur.toFixed(3)}ms`); -} - -document.dispatchEvent(new CustomEvent('gitea:index-ready')); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 2de29f52b9..e0b4a3e521 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -1,29 +1,188 @@ -// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors -import './bootstrap.ts'; +import '../fomantic/build/fomantic.js'; +import '../css/index.css'; +import type {HtmxResponseInfo} from 'htmx.org'; +import {showErrorToast} from './modules/toast.ts'; -// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) -// so load globals (including jQuery) as early as possible -import './globals.ts'; +import {initDashboardRepoList} from './features/dashboard.ts'; +import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; +import {initRepoGraphGit} from './features/repo-graph.ts'; +import {initHeatmap} from './features/heatmap.ts'; +import {initImageDiff} from './features/imagediff.ts'; +import {initRepoMigration} from './features/repo-migration.ts'; +import {initRepoProject} from './features/repo-projects.ts'; +import {initTableSort} from './features/tablesort.ts'; +import {initAdminUserListSearchForm} from './features/admin/users.ts'; +import {initAdminConfigs} from './features/admin/config.ts'; +import {initMarkupAnchors} from './markup/anchors.ts'; +import {initNotificationCount} from './features/notification.ts'; +import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; +import {initStopwatch} from './features/stopwatch.ts'; +import {initRepoFileSearch} from './features/repo-findfile.ts'; +import {initMarkupContent} from './markup/content.ts'; +import {initRepoFileView} from './features/file-view.ts'; +import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; +import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; +import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; +import {initRepoTopicBar} from './features/repo-home.ts'; +import {initAdminCommon} from './features/admin/common.ts'; +import {initRepoCodeView} from './features/repo-code.ts'; +import {initSshKeyFormParser} from './features/sshkey-helper.ts'; +import {initUserSettings} from './features/user-settings.ts'; +import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; +import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; +import {initRepoDiffView} from './features/repo-diff.ts'; +import {initOrgTeam} from './features/org-team.ts'; +import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts'; +import {initRepoReleaseNew} from './features/repo-release.ts'; +import {initRepoEditor} from './features/repo-editor.ts'; +import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts'; +import {initInstall} from './features/install.ts'; +import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts'; +import {initRepoBranchButton} from './features/repo-branch.ts'; +import {initCommonOrganization} from './features/common-organization.ts'; +import {initRepoWikiForm} from './features/repo-wiki.ts'; +import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts'; +import {initCopyContent} from './features/copycontent.ts'; +import {initCaptcha} from './features/captcha.ts'; +import {initRepositoryActionView} from './features/repo-actions.ts'; +import {initGlobalTooltips} from './modules/tippy.ts'; +import {initGiteaFomantic} from './modules/fomantic.ts'; +import {initSubmitEventPolyfill} from './utils/dom.ts'; +import {initRepoIssueList} from './features/repo-issue-list.ts'; +import {initCommonIssueListQuickGoto} from './features/common-issue-list.ts'; +import {initRepoContributors} from './features/contributors.ts'; +import {initRepoCodeFrequency} from './features/code-frequency.ts'; +import {initRepoRecentCommits} from './features/recent-commits.ts'; +import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts'; +import {initGlobalSelectorObserver} from './modules/observer.ts'; +import {initRepositorySearch} from './features/repo-search.ts'; +import {initColorPickers} from './features/colorpicker.ts'; +import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; +import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; +import {initGlobalFetchAction} from './features/common-fetch-action.ts'; +import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts'; +import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts'; +import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; +import {callInitFunctions} from './modules/init.ts'; +import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; +import {initActionsPermissionsForm} from './features/common-actions-permissions.ts'; +import {initGlobalShortcut} from './modules/shortcut.ts'; -import './webcomponents/index.ts'; -import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts -import {onDomReady} from './utils/dom.ts'; +const initStartTime = performance.now(); +const initPerformanceTracer = callInitFunctions([ + initSubmitEventPolyfill, + initGiteaFomantic, -// TODO: There is a bug in htmx, it incorrectly checks "readyState === 'complete'" when the DOM tree is ready and won't trigger DOMContentLoaded -// Then importing the htmx in our onDomReady will make htmx skip its initialization. -// If the bug would be fixed (https://github.com/bigskysoftware/htmx/pull/3365), then we can only import htmx in "onDomReady" -import 'htmx.org'; + initGlobalComponent, + initGlobalDropdown, + initGlobalFetchAction, + initGlobalTooltips, + initGlobalButtonClickOnEnter, + initGlobalButtons, + initGlobalCopyToClipboardListener, + initGlobalEnterQuickSubmit, + initGlobalFormDirtyLeaveConfirm, + initGlobalComboMarkdownEditor, + initGlobalDeleteButton, + initGlobalInput, + initGlobalShortcut, -onDomReady(async () => { - // when navigate before the import complete, there will be an error from webpack chunk loader: - // JavaScript promise rejection: Loading chunk index-domready failed. - try { - await import(/* webpackChunkName: "index-domready" */'./index-domready.ts'); - } catch (e) { - if (e.name === 'ChunkLoadError') { - console.error('Error loading index-domready:', e); - } else { - throw e; - } - } + initCommonOrganization, + initCommonIssueListQuickGoto, + + initCompSearchUserBox, + initCompWebHookEditor, + + initInstall, + + initCommmPageComponents, + + initHeatmap, + initImageDiff, + initMarkupAnchors, + initMarkupContent, + initSshKeyFormParser, + initStopwatch, + initTableSort, + initRepoFileSearch, + initCopyContent, + + initAdminCommon, + initAdminUserListSearchForm, + initAdminConfigs, + initAdminSelfCheck, + + initDashboardRepoList, + + initNotificationCount, + + initOrgTeam, + + initRepoActivityTopAuthorsChart, + initRepoArchiveLinks, + initRepoBranchButton, + initRepoCodeView, + initBranchSelectorTabs, + initRepoEllipsisButton, + initRepoDiffCommitBranchesAndTags, + initRepoEditor, + initRepoGraphGit, + initRepoIssueContentHistory, + initRepoIssueList, + initRepoIssueFilterItemLabel, + initRepoIssueSidebarDependency, + initRepoMigration, + initRepoMigrationStatusChecker, + initRepoProject, + initRepoPullRequestAllowMaintainerEdit, + initRepoPullRequestReview, + initRepoReleaseNew, + initRepoTopicBar, + initRepoViewFileTree, + initRepoWikiForm, + initRepository, + initRepositoryActionView, + initRepositorySearch, + initRepoContributors, + initRepoCodeFrequency, + initRepoRecentCommits, + + initCommitStatuses, + initCaptcha, + + initUserCheckAppUrl, + initUserAuthOauth2, + initUserAuthWebAuthn, + initUserAuthWebAuthnRegister, + initUserSettings, + initRepoDiffView, + initColorPickers, + + initOAuth2SettingsDisableCheckbox, + + initRepoFileView, + initActionsPermissionsForm, +]); + +// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions. +initGlobalSelectorObserver(initPerformanceTracer); +if (initPerformanceTracer) initPerformanceTracer.printResults(); + +const initDur = performance.now() - initStartTime; +if (initDur > 500) { + console.error(`slow init functions took ${initDur.toFixed(3)}ms`); +} + +// https://htmx.org/events/#htmx:sendError +type HtmxEvent = Event & {detail: HtmxResponseInfo}; +document.body.addEventListener('htmx:sendError', (event) => { + // TODO: add translations + showErrorToast(`Network error when calling ${(event as HtmxEvent).detail.requestConfig.path}`); }); +// https://htmx.org/events/#htmx:responseError +document.body.addEventListener('htmx:responseError', (event) => { + // TODO: add translations + showErrorToast(`Error ${(event as HtmxEvent).detail.xhr.status} when calling ${(event as HtmxEvent).detail.requestConfig.path}`); +}); + +document.dispatchEvent(new CustomEvent('gitea:index-ready')); diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts index 4596327876..90515e1363 100644 --- a/web_src/js/markup/asciicast.ts +++ b/web_src/js/markup/asciicast.ts @@ -3,8 +3,8 @@ import {queryElems} from '../utils/dom.ts'; export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise { queryElems(elMarkup, '.asciinema-player-container', async (el) => { const [player] = await Promise.all([ - import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), - import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), + import('asciinema-player'), + import('asciinema-player/dist/bundle/asciinema-player.css'), ]); player.create(el.getAttribute('data-asciinema-player-src')!, el, { diff --git a/web_src/js/markup/math.ts b/web_src/js/markup/math.ts index bc118137a1..a3ee102ccd 100644 --- a/web_src/js/markup/math.ts +++ b/web_src/js/markup/math.ts @@ -16,8 +16,8 @@ export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise { // .markup code.language-math' queryElems(elMarkup, 'code.language-math', async (el) => { const [{default: katex}] = await Promise.all([ - import(/* webpackChunkName: "katex" */'katex'), - import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), + import('katex'), + import('katex/dist/katex.css'), ]); const MAX_CHARS = 1000; diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 5148ff377c..aaf6da6805 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -72,8 +72,8 @@ export function sourceNeedsElk(source: string) { } async function loadMermaid(needElkRender: boolean) { - const mermaidPromise = import(/* webpackChunkName: "mermaid" */'mermaid'); - const elkPromise = needElkRender ? import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk') : null; + const mermaidPromise = import('mermaid'); + const elkPromise = needElkRender ? import('@mermaid-js/layout-elk') : null; const results = await Promise.all([mermaidPromise, elkPromise]); return { mermaid: results[0].default, diff --git a/web_src/js/markup/refissue.ts b/web_src/js/markup/refissue.ts index f2fcd24f39..b17f452dd4 100644 --- a/web_src/js/markup/refissue.ts +++ b/web_src/js/markup/refissue.ts @@ -20,7 +20,7 @@ function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { const el = document.createElement('div'); const onShowAsync = async () => { - const {default: ContextPopup} = await import(/* webpackChunkName: "ContextPopup" */ '../components/ContextPopup.vue'); + const {default: ContextPopup} = await import('../components/ContextPopup.vue'); const view = createApp(ContextPopup, { // backend: GetIssueInfo loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, diff --git a/web_src/js/bootstrap.test.ts b/web_src/js/modules/errors.test.ts similarity index 70% rename from web_src/js/bootstrap.test.ts rename to web_src/js/modules/errors.test.ts index 9d163ebbb8..c860a3f7cb 100644 --- a/web_src/js/bootstrap.test.ts +++ b/web_src/js/modules/errors.test.ts @@ -1,4 +1,4 @@ -import {showGlobalErrorMessage, shouldIgnoreError} from './bootstrap.ts'; +import {showGlobalErrorMessage, shouldIgnoreError} from './errors.ts'; test('showGlobalErrorMessage', () => { document.body.innerHTML = '
    '; @@ -13,9 +13,9 @@ test('showGlobalErrorMessage', () => { test('shouldIgnoreError', () => { for (const url of [ - 'https://gitea.test/assets/js/monaco.b359ef7e.js', - 'https://gitea.test/assets/js/monaco-editor.4a969118.worker.js', - 'https://gitea.test/assets/js/vendors-node_modules_pnpm_monaco-editor_0_55_1_node_modules_monaco-editor_esm_vs_base_common_-e11c7c.966a028d.js', + 'https://gitea.test/assets/js/monaco.D14TzjS9.js', + 'https://gitea.test/assets/js/editor.api2.BdhK7zNg.js', + 'https://gitea.test/assets/js/editor.worker.BYgvyFya.js', ]) { const err = new Error('test'); err.stack = `Error: test\n at ${url}:1:1`; diff --git a/web_src/js/modules/errors.ts b/web_src/js/modules/errors.ts new file mode 100644 index 0000000000..3ec01b3eb7 --- /dev/null +++ b/web_src/js/modules/errors.ts @@ -0,0 +1,67 @@ +// keep this file lightweight, it's imported into IIFE chunk in bootstrap +import {html} from '../utils/html.ts'; +import type {Intent} from '../types.ts'; + +export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { + const msgContainer = document.querySelector('.page-content') ?? document.body; + if (!msgContainer) { + alert(`${msgType}: ${msg}`); + return; + } + const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages + let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); + if (!msgDiv) { + const el = document.createElement('div'); + el.innerHTML = html`
    `; + msgDiv = el.childNodes[0] as HTMLDivElement; + } + // merge duplicated messages into "the message (count)" format + const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; + msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); + msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); + msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); + msgContainer.prepend(msgDiv); +} + +export function shouldIgnoreError(err: Error) { + const ignorePatterns: Array = [ + // https://github.com/go-gitea/gitea/issues/30861 + // https://github.com/microsoft/monaco-editor/issues/4496 + // https://github.com/microsoft/monaco-editor/issues/4679 + /\/assets\/js\/.*(monaco|editor\.(api|worker))/, + ]; + for (const pattern of ignorePatterns) { + if (pattern.test(err.stack ?? '')) return true; + } + return false; +} + +export function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) { + const err = error ?? reason; + const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin)); + const {runModeIsProd} = window.config ?? {}; + + // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a + // non-critical event from the browser. We log them but don't show them to users. Examples: + // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors + // - https://github.com/mozilla-mobile/firefox-ios/issues/10817 + // - https://github.com/go-gitea/gitea/issues/20240 + if (!err) { + if (message) console.error(new Error(message)); + if (runModeIsProd) return; + } + + if (err instanceof Error) { + // If the error stack trace does not include the base URL of our script assets, it likely came + // from a browser extension or inline script. Do not show such errors in production. + if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return; + // Ignore some known errors that are unable to fix + if (shouldIgnoreError(err)) return; + } + + let msg = err?.message ?? message; + if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`; + const dot = msg.endsWith('.') ? '' : '.'; + const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type; + showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`); +} diff --git a/web_src/js/modules/fomantic.ts b/web_src/js/modules/fomantic.ts index 4b1dbc4f62..ee45f676ba 100644 --- a/web_src/js/modules/fomantic.ts +++ b/web_src/js/modules/fomantic.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {initAriaCheckboxPatch} from './fomantic/checkbox.ts'; import {initAriaFormFieldPatch} from './fomantic/form.ts'; import {initAriaDropdownPatch} from './fomantic/dropdown.ts'; diff --git a/web_src/js/modules/fomantic/base.ts b/web_src/js/modules/fomantic/base.ts index a227d8123a..f3953e60cd 100644 --- a/web_src/js/modules/fomantic/base.ts +++ b/web_src/js/modules/fomantic/base.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {generateElemId} from '../../utils/dom.ts'; export function linkLabelAndInput(label: Element, input: Element) { diff --git a/web_src/js/modules/fomantic/dimmer.ts b/web_src/js/modules/fomantic/dimmer.ts index cbdfac23cb..6782f0137d 100644 --- a/web_src/js/modules/fomantic/dimmer.ts +++ b/web_src/js/modules/fomantic/dimmer.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {queryElemChildren} from '../../utils/dom.ts'; export function initFomanticDimmer() { diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 7f7f3611be..b98a5cf3f4 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import type {FomanticInitFunction} from '../../types.ts'; import {generateElemId, queryElems} from '../../utils/dom.ts'; diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts index a96c7785e1..1383692c98 100644 --- a/web_src/js/modules/fomantic/modal.ts +++ b/web_src/js/modules/fomantic/modal.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import type {FomanticInitFunction} from '../../types.ts'; import {queryElems} from '../../utils/dom.ts'; import {hideToastsFrom} from '../toast.ts'; diff --git a/web_src/js/modules/fomantic/tab.ts b/web_src/js/modules/fomantic/tab.ts index b9578c9637..4d1bd7e648 100644 --- a/web_src/js/modules/fomantic/tab.ts +++ b/web_src/js/modules/fomantic/tab.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {queryElemSiblings} from '../../utils/dom.ts'; export function initFomanticTab() { diff --git a/web_src/js/modules/fomantic/transition.ts b/web_src/js/modules/fomantic/transition.ts index 52c407c9c0..c4eb1d75e9 100644 --- a/web_src/js/modules/fomantic/transition.ts +++ b/web_src/js/modules/fomantic/transition.ts @@ -1,5 +1,3 @@ -import $ from 'jquery'; - export function initFomanticTransition() { const transitionNopBehaviors = new Set([ 'clear queue', 'stop', 'stop all', 'destroy', diff --git a/web_src/js/modules/monaco.ts b/web_src/js/modules/monaco.ts new file mode 100644 index 0000000000..c8e1ff7765 --- /dev/null +++ b/web_src/js/modules/monaco.ts @@ -0,0 +1,17 @@ +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; +import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; +import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; +import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; + +window.MonacoEnvironment = { + getWorker(_: string, label: string) { + if (label === 'json') return new jsonWorker(); + if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker(); + if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker(); + if (label === 'typescript' || label === 'javascript') return new tsWorker(); + return new editorWorker(); + }, +}; + +export * from 'monaco-editor'; diff --git a/web_src/js/modules/sortable.ts b/web_src/js/modules/sortable.ts index f3515fcb8d..c49f36ba8b 100644 --- a/web_src/js/modules/sortable.ts +++ b/web_src/js/modules/sortable.ts @@ -3,7 +3,7 @@ import type SortableType from 'sortablejs'; export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}): Promise { // type reassigned because typescript derives the wrong type from this import - const {Sortable} = (await import(/* webpackChunkName: "sortablejs" */'sortablejs') as unknown as {Sortable: typeof SortableType}); + const {Sortable} = (await import('sortablejs') as unknown as {Sortable: typeof SortableType}); return new Sortable(el, { animation: 150, diff --git a/web_src/js/modules/worker.ts b/web_src/js/modules/worker.ts index b730e30bb2..64c32fbe81 100644 --- a/web_src/js/modules/worker.ts +++ b/web_src/js/modules/worker.ts @@ -1,11 +1,11 @@ -const {appSubUrl, assetVersionEncoded} = window.config; +const {appSubUrl, sharedWorkerUri} = window.config; export class UserEventsSharedWorker { sharedWorker: SharedWorker; // options can be either a string (the debug name of the worker) or an object of type WorkerOptions constructor(options?: string | WorkerOptions) { - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, options); + const worker = new SharedWorker(sharedWorkerUri, options); this.sharedWorker = worker; worker.addEventListener('error', (event) => { console.error('worker error', event); diff --git a/web_src/js/render/plugins/3d-viewer.ts b/web_src/js/render/plugins/3d-viewer.ts index 6f3ee15d26..f997790af6 100644 --- a/web_src/js/render/plugins/3d-viewer.ts +++ b/web_src/js/render/plugins/3d-viewer.ts @@ -47,7 +47,7 @@ export function newRenderPlugin3DViewer(): FileRenderPlugin { async render(container: HTMLElement, fileUrl: string): Promise { // TODO: height and/or max-height? - const OV = await import(/* webpackChunkName: "online-3d-viewer" */'online-3d-viewer'); + const OV = await import('online-3d-viewer'); const viewer = new OV.EmbeddedViewer(container, { backgroundColor: new OV.RGBAColor(59, 68, 76, 0), defaultColor: new OV.RGBColor(65, 131, 196), diff --git a/web_src/js/render/plugins/pdf-viewer.ts b/web_src/js/render/plugins/pdf-viewer.ts index 40623be055..c7040e96ef 100644 --- a/web_src/js/render/plugins/pdf-viewer.ts +++ b/web_src/js/render/plugins/pdf-viewer.ts @@ -9,7 +9,7 @@ export function newRenderPluginPdfViewer(): FileRenderPlugin { }, async render(container: HTMLElement, fileUrl: string): Promise { - const PDFObject = await import(/* webpackChunkName: "pdfobject" */'pdfobject'); + const PDFObject = await import('pdfobject'); // TODO: the PDFObject library does not support dynamic height adjustment, container.style.height = `${window.innerHeight - 100}px`; if (!PDFObject.default.embed(fileUrl, container)) { diff --git a/web_src/js/standalone/devtest.ts b/web_src/js/standalone/devtest.ts index 39c41db042..20ab163d1a 100644 --- a/web_src/js/standalone/devtest.ts +++ b/web_src/js/standalone/devtest.ts @@ -1,3 +1,4 @@ +import '../../css/standalone/devtest.css'; import {showInfoToast, showWarningToast, showErrorToast, type Toast} from '../modules/toast.ts'; type LevelMap = Record Toast | null>; diff --git a/web_src/js/standalone/external-render-iframe.ts b/web_src/js/standalone/external-render-iframe.ts index f8ec070785..3b489f8ee3 100644 --- a/web_src/js/standalone/external-render-iframe.ts +++ b/web_src/js/standalone/external-render-iframe.ts @@ -11,6 +11,8 @@ RENDER_COMMAND = `echo '
    ('[role="menuitem"]'); if (e.shiftKey) { if (document.activeElement === items[0]) { e.preventDefault(); @@ -39,7 +62,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { } else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); - this.button?._tippy.hide(); + this.hidePopup(); this.button?.focus(); } else if (e.key === ' ' || e.code === 'Enter') { if (document.activeElement?.matches('[role="menuitem"]')) { @@ -48,20 +71,20 @@ window.customElements.define('overflow-menu', class extends HTMLElement { (document.activeElement as HTMLElement).click(); } } else if (e.key === 'ArrowDown') { - if (document.activeElement?.matches('.tippy-target')) { + if (document.activeElement === this.popup) { e.preventDefault(); e.stopPropagation(); - document.activeElement.querySelector('[role="menuitem"]:first-of-type')?.focus(); + this.popup.querySelector('[role="menuitem"]:first-of-type')?.focus(); } else if (document.activeElement?.matches('[role="menuitem"]')) { e.preventDefault(); e.stopPropagation(); (document.activeElement.nextElementSibling as HTMLElement)?.focus(); } } else if (e.key === 'ArrowUp') { - if (document.activeElement?.matches('.tippy-target')) { + if (document.activeElement === this.popup) { e.preventDefault(); e.stopPropagation(); - document.activeElement.querySelector('[role="menuitem"]:last-of-type')?.focus(); + this.popup.querySelector('[role="menuitem"]:last-of-type')?.focus(); } else if (document.activeElement?.matches('[role="menuitem"]')) { e.preventDefault(); e.stopPropagation(); @@ -69,16 +92,15 @@ window.customElements.define('overflow-menu', class extends HTMLElement { } } }); - div.classList.add('tippy-target'); - this.handleItemClick(div, '.tippy-target > .item'); - this.tippyContent = div; - } // end if: no tippyContent and create a new one + this.handleItemClick(div, '.overflow-menu-popup > .item'); + this.popup = div; + } // end if: no popup and create a new one const itemFlexSpace = this.menuItemsEl.querySelector('.item-flex-space'); const itemOverFlowMenuButton = this.querySelector('.overflow-menu-button'); - // move items in tippy back into the menu items for subsequent measurement - for (const item of this.tippyItems || []) { + // move items in popup back into the menu items for subsequent measurement + for (const item of this.overflowItems || []) { if (!itemFlexSpace || item.getAttribute('data-after-flex-space')) { this.menuItemsEl.append(item); } else { @@ -90,7 +112,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { // flex space and overflow menu are excluded from measurement itemFlexSpace?.style.setProperty('display', 'none', 'important'); itemOverFlowMenuButton?.style.setProperty('display', 'none', 'important'); - this.tippyItems = []; + this.overflowItems = []; const menuRight = this.offsetLeft + this.offsetWidth; const menuItems = this.menuItemsEl.querySelectorAll('.item, .item-flex-space'); let afterFlexSpace = false; @@ -102,64 +124,64 @@ window.customElements.define('overflow-menu', class extends HTMLElement { if (afterFlexSpace) item.setAttribute('data-after-flex-space', 'true'); const itemRight = item.offsetLeft + item.offsetWidth; if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button with some extra space - const onlyLastItem = idx === menuItems.length - 1 && this.tippyItems.length === 0; + const onlyLastItem = idx === menuItems.length - 1 && this.overflowItems.length === 0; const lastItemFit = onlyLastItem && menuRight - itemRight > 0; const moveToPopup = !onlyLastItem || !lastItemFit; - if (moveToPopup) this.tippyItems.push(item); + if (moveToPopup) this.overflowItems.push(item); } } itemFlexSpace?.style.removeProperty('display'); itemOverFlowMenuButton?.style.removeProperty('display'); // if there are no overflown items, remove any previously created button - if (!this.tippyItems?.length) { - const btn = this.querySelector('.overflow-menu-button'); - btn?._tippy?.destroy(); - btn?.remove(); + if (!this.overflowItems?.length) { + this.hidePopup(); + this.button?.remove(); + this.popup?.remove(); this.button = null; return; } - // remove aria role from items that moved from tippy to menu + // remove aria role from items that moved from popup to menu for (const item of menuItems) { - if (!this.tippyItems.includes(item)) { + if (!this.overflowItems.includes(item)) { item.removeAttribute('role'); } } - // move all items that overflow into tippy - for (const item of this.tippyItems) { + // move all items that overflow into popup + for (const item of this.overflowItems) { item.setAttribute('role', 'menuitem'); - this.tippyContent.append(item); + this.popup.append(item); } - // update existing tippy - if (this.button?._tippy) { - this.button._tippy.setContent(this.tippyContent); + // update existing popup + if (this.button) { this.updateButtonActivationState(); return; } - // create button initially + // create button and attach popup + const popupId = generateElemId('overflow-popup-'); + this.popup.id = popupId; + this.button = document.createElement('button'); this.button.classList.add('overflow-menu-button'); this.button.setAttribute('aria-label', window.config.i18n.more_items); + this.button.setAttribute('aria-haspopup', 'true'); + this.button.setAttribute('aria-expanded', 'false'); + this.button.setAttribute('aria-controls', popupId); this.button.innerHTML = octiconKebabHorizontal; - this.append(this.button); - createTippy(this.button, { - trigger: 'click', - hideOnClick: true, - interactive: true, - placement: 'bottom-end', - role: 'menu', - theme: 'menu', - content: this.tippyContent, - onShow: () => { // FIXME: onShown doesn't work (never be called) - setTimeout(() => { - this.tippyContent.focus(); - }, 0); - }, + this.button.addEventListener('click', (e) => { + e.stopPropagation(); + if (this.popup.style.display === 'none') { + this.showPopup(); + } else { + this.hidePopup(); + } }); + this.append(this.button); + this.append(this.popup); this.updateButtonActivationState(); }); @@ -202,7 +224,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { handleItemClick(el: Element, selector: string) { addDelegatedEventListener(el, 'click', selector, () => { - this.button?._tippy?.hide(); + this.hidePopup(); this.updateButtonActivationState(); }); } @@ -239,5 +261,6 @@ window.customElements.define('overflow-menu', class extends HTMLElement { disconnectedCallback() { this.mutationObserver?.disconnect(); this.resizeObserver?.disconnect(); + document.removeEventListener('click', this.onClickOutside, true); } }); diff --git a/webpack.config.ts b/webpack.config.ts deleted file mode 100644 index e3ef996909..0000000000 --- a/webpack.config.ts +++ /dev/null @@ -1,268 +0,0 @@ -import wrapAnsi from 'wrap-ansi'; -import AddAssetPlugin from 'add-asset-webpack-plugin'; -import LicenseCheckerWebpackPlugin from '@techknowlogick/license-checker-webpack-plugin'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; -import {VueLoaderPlugin} from 'vue-loader'; -import {EsbuildPlugin} from 'esbuild-loader'; -import {parse} from 'node:path'; -import webpack, {type Configuration, type EntryObject} from 'webpack'; -import {fileURLToPath} from 'node:url'; -import {readFileSync, globSync} from 'node:fs'; -import {env} from 'node:process'; -import tailwindcss from 'tailwindcss'; -import tailwindConfig from './tailwind.config.ts'; - -const {SourceMapDevToolPlugin, DefinePlugin, EnvironmentPlugin} = webpack; -const formatLicenseText = (licenseText: string) => wrapAnsi(licenseText || '', 80).trim(); - -const themes: EntryObject = {}; -for (const path of globSync('web_src/css/themes/*.css', {cwd: import.meta.dirname})) { - themes[parse(path).name] = [`./${path}`]; -} - -const isProduction = env.NODE_ENV !== 'development'; - -// ENABLE_SOURCEMAP accepts the following values: -// true - all enabled, the default in development -// reduced - minimal sourcemaps, the default in production -// false - all disabled -let sourceMaps; -if ('ENABLE_SOURCEMAP' in env) { - sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP || '') ? env.ENABLE_SOURCEMAP : 'reduced'; -} else { - sourceMaps = isProduction ? 'reduced' : 'true'; -} - -// define which web components we use for Vue to not interpret them as Vue components -const webComponents = new Set([ - // our own, in web_src/js/webcomponents - 'overflow-menu', - 'origin-url', - // from dependencies - 'markdown-toolbar', - 'relative-time', - 'text-expander', -]); - -const filterCssImport = (url: string, ...args: Array) => { - const cssFile = args[1] || args[0]; // resourcePath is 2nd argument for url and 3rd for import - const importedFile = url.replace(/[?#].+/, '').toLowerCase(); - - if (cssFile.includes('fomantic')) { - if (importedFile.includes('brand-icons')) return false; - if (/(eot|ttf|otf|woff|svg)$/i.test(importedFile)) return false; - } - - if (cssFile.includes('katex') && /(ttf|woff)$/i.test(importedFile)) { - return false; - } - - return true; -}; - -export default { - mode: isProduction ? 'production' : 'development', - entry: { - index: [ - fileURLToPath(new URL('web_src/js/index.ts', import.meta.url)), - fileURLToPath(new URL('web_src/css/index.css', import.meta.url)), - ], - swagger: [ - fileURLToPath(new URL('web_src/js/standalone/swagger.ts', import.meta.url)), - fileURLToPath(new URL('web_src/css/standalone/swagger.css', import.meta.url)), - ], - 'external-render-iframe': [ - fileURLToPath(new URL('web_src/js/standalone/external-render-iframe.ts', import.meta.url)), - fileURLToPath(new URL('web_src/css/standalone/external-render-iframe.css', import.meta.url)), - ], - 'eventsource.sharedworker': [ - fileURLToPath(new URL('web_src/js/features/eventsource.sharedworker.ts', import.meta.url)), - ], - ...(!isProduction && { - devtest: [ - fileURLToPath(new URL('web_src/js/standalone/devtest.ts', import.meta.url)), - fileURLToPath(new URL('web_src/css/standalone/devtest.css', import.meta.url)), - ], - }), - ...themes, - }, - devtool: false, - output: { - path: fileURLToPath(new URL('public/assets', import.meta.url)), - filename: 'js/[name].js', - chunkFilename: 'js/[name].[contenthash:8].js', - }, - optimization: { - minimize: isProduction, - minimizer: [ - new EsbuildPlugin({ - target: 'es2020', - minify: true, - css: true, - legalComments: 'none', - }), - ], - moduleIds: 'named', - chunkIds: 'named', - }, - module: { - rules: [ - { - test: /\.vue$/i, - exclude: /node_modules/, - loader: 'vue-loader', - options: { - compilerOptions: { - isCustomElement: (tag: string) => webComponents.has(tag), - }, - }, - }, - { - test: /\.js$/i, - exclude: /node_modules/, - use: [ - { - loader: 'esbuild-loader', - options: { - loader: 'js', - target: 'es2020', - }, - }, - ], - }, - { - test: /\.ts$/i, - exclude: /node_modules/, - use: [ - { - loader: 'esbuild-loader', - options: { - loader: 'ts', - target: 'es2020', - }, - }, - ], - }, - { - test: /\.css$/i, - use: [ - { - loader: MiniCssExtractPlugin.loader, - }, - { - loader: 'css-loader', - options: { - sourceMap: sourceMaps === 'true', - url: {filter: filterCssImport}, - import: {filter: filterCssImport}, - importLoaders: 1, - }, - }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: [ - tailwindcss(tailwindConfig), - ], - }, - }, - }, - ], - }, - { - test: /\.svg$/i, - include: fileURLToPath(new URL('public/assets/img/svg', import.meta.url)), - type: 'asset/source', - }, - { - test: /\.(ttf|woff2?)$/i, - type: 'asset/resource', - generator: { - filename: 'fonts/[name].[contenthash:8][ext]', - }, - }, - ], - }, - plugins: [ - new DefinePlugin({ - __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API - __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production - __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // https://github.com/vuejs/vue-cli/pull/7443 - }), - // all environment variables used in bundled js via process.env must be declared here - new EnvironmentPlugin({ - TEST: 'false', - }), - new VueLoaderPlugin(), - new MiniCssExtractPlugin({ - filename: 'css/[name].css', - chunkFilename: 'css/[name].[contenthash:8].css', - }), - sourceMaps !== 'false' && new SourceMapDevToolPlugin({ - filename: '[file].[contenthash:8].map', - ...(sourceMaps === 'reduced' && {include: /^js\/index\.js$/}), - }), - new MonacoWebpackPlugin({ - filename: 'js/monaco-[name].[contenthash:8].worker.js', - }), - isProduction ? new LicenseCheckerWebpackPlugin({ - outputFilename: 'licenses.txt', - outputWriter: ({dependencies}: {dependencies: Array>}) => { - const line = '-'.repeat(80); - const goJson = readFileSync('assets/go-licenses.json', 'utf8'); - const goModules = JSON.parse(goJson).map(({name, licenseText}: Record) => { - return {name, body: formatLicenseText(licenseText)}; - }); - const jsModules = dependencies.map(({name, version, licenseName, licenseText}) => { - return {name, version, licenseName, body: formatLicenseText(licenseText)}; - }); - - const modules = [...goModules, ...jsModules].sort((a, b) => a.name.localeCompare(b.name)); - return modules.map(({name, version, licenseName, body}) => { - const title = licenseName ? `${name}@${version} - ${licenseName}` : name; - return `${line}\n${title}\n${line}\n${body}`; - }).join('\n'); - }, - override: { - 'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33 - }, - emitError: true, - allow: '(Apache-2.0 OR 0BSD OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)', - }) : new AddAssetPlugin('licenses.txt', `Licenses are disabled during development`), - ], - performance: { - hints: false, - maxEntrypointSize: Infinity, - maxAssetSize: Infinity, - }, - resolve: { - symlinks: true, - modules: ['node_modules'], - }, - watchOptions: { - ignored: [ - 'node_modules/**', - ], - }, - stats: { - assetsSort: 'name', - assetsSpace: Infinity, - cached: false, - cachedModules: false, - children: false, - chunkModules: false, - chunkOrigins: false, - chunksSort: 'name', - colors: true, - entrypoints: false, - groupAssetsByChunk: false, - groupAssetsByEmitStatus: false, - groupAssetsByInfo: false, - groupModulesByAttributes: false, - modules: false, - reasons: false, - runtimeModules: false, - }, -} satisfies Configuration; From 755d200371a5030fac2824085c527ed6a181ae04 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 29 Mar 2026 18:57:39 +0200 Subject: [PATCH 138/207] Update AI Contribution Policy (#37022) I tried to tighten the AI contribution policy and make the expectations around AI-assisted submissions clearer. --------- Signed-off-by: silverwind Co-authored-by: Giteabot Co-authored-by: silverwind --- CONTRIBUTING.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33b329182c..856515a34e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,16 +70,18 @@ For configuring IDEs for Gitea development, see the [contributed IDE configurati ## AI Contribution Policy -Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly. +Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly and disclose that use clearly. -1. Include related issues or pull requests in the prompt so that the AI has ideal context. -2. Review AI-generated code closely before submitting a pull request. -3. Manually test the changes and add appropriate automated tests where feasible. -4. Only use AI to assist in contributions that you understand well enough to respond to feedback without relying on AI. -5. Indicate AI-generated content in issue and pull requests descriptions and comments. Specify which model was used. -6. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model. +1. Review AI-generated code closely before marking a pull request ready for review. +2. Manually test the changes and add appropriate automated tests where feasible. +3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review. +4. Disclose AI-assisted content clearly. +5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model. +6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit. -Maintainers reserve the right to close pull requests and issues that appear to be low-quality AI-generated content. We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process. +Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves. + +We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process. ## Issues From a88449f13ff08319ea923fb26cf192ea7dbdf16f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 30 Mar 2026 01:39:15 +0800 Subject: [PATCH 139/207] Fix various problems (#37029) 1. Use "margin/padding inline" * Fix #37027 2. Make DetectWellKnownMimeType fallback to system mime types 3. Make catFileBatchCommunicator close pipes * Old behavior in 1.25: https://github.com/go-gitea/gitea/blob/release/v1.25/modules/git/batch_reader.go#L45-L55 * Try to fix #37028 --- modules/git/catfile_batch_reader.go | 27 +++++++++------ modules/public/mime_types.go | 52 +++++++++++++++++------------ web_src/css/markup/content.css | 25 +++++++------- 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/modules/git/catfile_batch_reader.go b/modules/git/catfile_batch_reader.go index 8a0b342079..0c8fc740be 100644 --- a/modules/git/catfile_batch_reader.go +++ b/modules/git/catfile_batch_reader.go @@ -22,16 +22,16 @@ import ( var catFileBatchDebugWaitClose atomic.Int64 type catFileBatchCommunicator struct { - cancel context.CancelFunc + closeFunc func(err error) reqWriter io.Writer respReader *bufio.Reader debugGitCmd *gitcmd.Command } func (b *catFileBatchCommunicator) Close() { - if b.cancel != nil { - b.cancel() - b.cancel = nil + if b.closeFunc != nil { + b.closeFunc(nil) + b.closeFunc = nil } } @@ -47,10 +47,19 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co } stdPipeClose() } + closeFunc := func(err error) { + ctxCancel(err) + pipeClose() + } + return newCatFileBatchWithCloseFunc(ctx, repoPath, cmdCatFile, stdinWriter, stdoutReader, closeFunc) +} - ret = &catFileBatchCommunicator{ +func newCatFileBatchWithCloseFunc(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command, + stdinWriter gitcmd.PipeWriter, stdoutReader gitcmd.PipeReader, closeFunc func(err error), +) *catFileBatchCommunicator { + ret := &catFileBatchCommunicator{ debugGitCmd: cmdCatFile, - cancel: func() { ctxCancel(nil) }, + closeFunc: closeFunc, reqWriter: stdinWriter, respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations } @@ -60,8 +69,7 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err) // ideally here it should return the error, but it would require refactoring all callers // so just return a dummy communicator that does nothing, almost the same behavior as before, not bad - ctxCancel(err) - pipeClose() + closeFunc(err) return ret } @@ -70,8 +78,7 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co if err != nil && !errors.Is(err, context.Canceled) { log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err) } - ctxCancel(err) - pipeClose() + closeFunc(err) }() return ret diff --git a/modules/public/mime_types.go b/modules/public/mime_types.go index fef85d77cb..fa4691c6a9 100644 --- a/modules/public/mime_types.go +++ b/modules/public/mime_types.go @@ -4,31 +4,36 @@ package public import ( + "mime" "strings" + "sync" ) -// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, see the comment of DetectWellKnownMimeType -var wellKnownMimeTypesLower = map[string]string{ - ".avif": "image/avif", - ".css": "text/css; charset=utf-8", - ".gif": "image/gif", - ".htm": "text/html; charset=utf-8", - ".html": "text/html; charset=utf-8", - ".jpeg": "image/jpeg", - ".jpg": "image/jpeg", - ".js": "text/javascript; charset=utf-8", - ".json": "application/json", - ".mjs": "text/javascript; charset=utf-8", - ".pdf": "application/pdf", - ".png": "image/png", - ".svg": "image/svg+xml", - ".wasm": "application/wasm", - ".webp": "image/webp", - ".xml": "text/xml; charset=utf-8", +// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, +// see the comment of DetectWellKnownMimeType +var wellKnownMimeTypesLower = sync.OnceValue(func() map[string]string { + return map[string]string{ + ".avif": "image/avif", + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".htm": "text/html; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json", + ".mjs": "text/javascript; charset=utf-8", + ".pdf": "application/pdf", + ".png": "image/png", + ".svg": "image/svg+xml", + ".wasm": "application/wasm", + ".webp": "image/webp", + ".xml": "text/xml; charset=utf-8", - // well, there are some types missing from the builtin list - ".txt": "text/plain; charset=utf-8", -} + // well, there are some types missing from the builtin list + ".txt": "text/plain; charset=utf-8", + } +}) // DetectWellKnownMimeType will return the mime-type for a well-known file ext name // The purpose of this function is to bypass the unstable behavior of Golang's mime.TypeByExtension @@ -38,5 +43,8 @@ var wellKnownMimeTypesLower = map[string]string{ // DetectWellKnownMimeType makes the Content-Type for well-known files stable. func DetectWellKnownMimeType(ext string) string { ext = strings.ToLower(ext) - return wellKnownMimeTypesLower[ext] + if s, ok := wellKnownMimeTypesLower()[ext]; ok { + return s + } + return mime.TypeByExtension(ext) } diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 6ca6f95c69..c86510d5cf 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -24,8 +24,8 @@ .markup .anchor { float: left; - padding-right: 4px; - margin-left: -20px; + padding-inline-end: 4px; + margin-inline-start: -20px; color: inherit; } @@ -151,7 +151,7 @@ In markup content, we always use bottom margin for all elements */ .markup ul, .markup ol { - padding-left: 2em; + padding-inline-start: 2em; } .markup ul.no-list, @@ -173,13 +173,14 @@ In markup content, we always use bottom margin for all elements */ } .markup .task-list-item input[type="checkbox"] { - margin: 0 .6em .25em -1.4em; + margin-bottom: 0.25em; + margin-inline: -1.4em 0.6em; vertical-align: middle; padding: 0; } .markup .task-list-item input[type="checkbox"] + p { - margin-left: -0.2em; + margin-inline-start: -0.2em; display: inline; } @@ -192,7 +193,7 @@ In markup content, we always use bottom margin for all elements */ } .markup input[type="checkbox"] { - margin-right: .25em; + margin-inline-end: .25em; margin-bottom: .25em; cursor: default; opacity: 1 !important; /* override fomantic on edit preview */ @@ -239,7 +240,7 @@ In markup content, we always use bottom margin for all elements */ } .markup blockquote { - margin-left: 0; + margin-inline-start: 0; padding: 0 15px; color: var(--color-text-light-2); border-left: 0.25em solid var(--color-secondary); @@ -318,12 +319,12 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { .markup img[align="right"], .markup video[align="right"] { - padding-left: 20px; + padding-inline-start: 20px; } .markup img[align="left"], .markup video[align="left"] { - padding-right: 28px; + padding-inline-end: 28px; } .markup span.frame { @@ -395,7 +396,7 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { .markup span.float-left { display: block; float: left; - margin-right: 13px; + margin-inline-end: 13px; overflow: hidden; } @@ -406,7 +407,7 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { .markup span.float-right { display: block; float: right; - margin-left: 13px; + margin-inline-start: 13px; overflow: hidden; } @@ -508,7 +509,7 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { .markup .ui.list .list, .markup ol.ui.list ol, .markup ul.ui.list ul { - padding-left: 2em; + padding-inline-start: 2em; } .markup details.frontmatter-content summary { From da51d5af1a49bf654fc5952083875a624086da32 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 29 Mar 2026 20:12:46 +0200 Subject: [PATCH 140/207] Add support for in_progress event in workflow_run webhook (#36979) With Gitea 1.25.4 the workflow event for in_progress was not triggered for Gitea Actions. Fixes #36906 --------- Co-authored-by: Claude Sonnet 4.6 --- services/actions/task.go | 5 ++ tests/integration/repo_webhook_test.go | 104 ++++++++++++++++--------- 2 files changed, 74 insertions(+), 35 deletions(-) diff --git a/services/actions/task.go b/services/actions/task.go index a21b600998..2cb10b6cd8 100644 --- a/services/actions/task.go +++ b/services/actions/task.go @@ -103,6 +103,11 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv CreateCommitStatusForRunJobs(ctx, job.Run, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, actionTask) + // job.Run is loaded inside the transaction before UpdateRunJob sets run.Started, + // so Started is zero only on the very first pick-up of that run. + if job.Run.Started.IsZero() { + NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + } return task, true, nil } diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 9ac9cced70..4b72962d4f 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -1401,7 +1401,10 @@ jobs: assert.Equal(t, commitID, webhookData.payloads[0].WorkflowRun.HeadSha) assert.Equal(t, "repo1", webhookData.payloads[0].Repo.Name) assert.Equal(t, "user2/repo1", webhookData.payloads[0].Repo.FullName) + runID := webhookData.payloads[0].WorkflowRun.ID + // The first runner to pick up a task fires in_progress (Started.IsZero() is true only once per run). + // The second runner picking up an independent job does not fire another in_progress event. for _, runner := range runners { task := runner.fetchTask(t) runner.execTask(t, task, &mockTaskOutcome{ @@ -1411,38 +1414,51 @@ jobs: // Call cancel ui api // Only a web UI API exists for cancelling workflow runs, so use the UI endpoint. - cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", webhookData.payloads[0].WorkflowRun.ID) + cancelURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/cancel", runID) req := NewRequest(t, "POST", cancelURL) session.MakeRequest(t, req, http.StatusOK) - assert.Len(t, webhookData.payloads, 2) + assert.Len(t, webhookData.payloads, 3) - // 4. Validate the second webhook payload + // 4. Validate the second webhook payload (in_progress, fired when the first runner picked up a job) assert.Equal(t, "workflow_run", webhookData.triggeredEvent) - assert.Equal(t, "completed", webhookData.payloads[1].Action) + assert.Equal(t, "in_progress", webhookData.payloads[1].Action) + assert.Equal(t, "in_progress", webhookData.payloads[1].WorkflowRun.Status) assert.Equal(t, "push", webhookData.payloads[1].WorkflowRun.Event) - assert.Equal(t, "completed", webhookData.payloads[1].WorkflowRun.Status) + assert.Equal(t, runID, webhookData.payloads[1].WorkflowRun.ID) assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[1].WorkflowRun.HeadBranch) assert.Equal(t, commitID, webhookData.payloads[1].WorkflowRun.HeadSha) assert.Equal(t, "repo1", webhookData.payloads[1].Repo.Name) assert.Equal(t, "user2/repo1", webhookData.payloads[1].Repo.FullName) - // Call rerun ui api - // Only a web UI API exists for rerunning workflow runs, so use the UI endpoint. - rerunURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/rerun", webhookData.payloads[0].WorkflowRun.ID) - req = NewRequest(t, "POST", rerunURL) - session.MakeRequest(t, req, http.StatusOK) - - assert.Len(t, webhookData.payloads, 3) - - // 5. Validate the third webhook payload + // 5. Validate the third webhook payload (completed, fired after cancel) assert.Equal(t, "workflow_run", webhookData.triggeredEvent) - assert.Equal(t, "requested", webhookData.payloads[2].Action) - assert.Equal(t, "queued", webhookData.payloads[2].WorkflowRun.Status) + assert.Equal(t, "completed", webhookData.payloads[2].Action) + assert.Equal(t, "push", webhookData.payloads[2].WorkflowRun.Event) + assert.Equal(t, "completed", webhookData.payloads[2].WorkflowRun.Status) + assert.Equal(t, runID, webhookData.payloads[2].WorkflowRun.ID) assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[2].WorkflowRun.HeadBranch) assert.Equal(t, commitID, webhookData.payloads[2].WorkflowRun.HeadSha) assert.Equal(t, "repo1", webhookData.payloads[2].Repo.Name) assert.Equal(t, "user2/repo1", webhookData.payloads[2].Repo.FullName) + + // Call rerun ui api + // Only a web UI API exists for rerunning workflow runs, so use the UI endpoint. + rerunURL := fmt.Sprintf("/user2/repo1/actions/runs/%d/rerun", runID) + req = NewRequest(t, "POST", rerunURL) + session.MakeRequest(t, req, http.StatusOK) + + assert.Len(t, webhookData.payloads, 4) + + // 6. Validate the fourth webhook payload (requested, fired after rerun) + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Equal(t, "requested", webhookData.payloads[3].Action) + assert.Equal(t, "queued", webhookData.payloads[3].WorkflowRun.Status) + assert.Equal(t, "push", webhookData.payloads[3].WorkflowRun.Event) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[3].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[3].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[3].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[3].Repo.FullName) } func testWorkflowRunEventsOnCancellingAbandonedRun(t *testing.T, webhookData *workflowRunWebhook, allJobsAbandoned bool) { @@ -1572,13 +1588,28 @@ jobs: err = actions.CancelAbandonedJobs(ctx) assert.NoError(t, err) - assert.Len(t, webhookData.payloads, 2) - assert.Equal(t, "completed", webhookData.payloads[1].Action) - assert.Equal(t, "completed", webhookData.payloads[1].WorkflowRun.Status) - assert.Equal(t, testRepo.DefaultBranch, webhookData.payloads[1].WorkflowRun.HeadBranch) - assert.Equal(t, commitID, webhookData.payloads[1].WorkflowRun.HeadSha) - assert.Equal(t, repoName, webhookData.payloads[1].Repo.Name) - assert.Equal(t, "user2/"+repoName, webhookData.payloads[1].Repo.FullName) + + if allJobsAbandoned { + // No runner picked up any task, so no in_progress event was fired. + assert.Len(t, webhookData.payloads, 2) + assert.Equal(t, "completed", webhookData.payloads[1].Action) + assert.Equal(t, "completed", webhookData.payloads[1].WorkflowRun.Status) + assert.Equal(t, testRepo.DefaultBranch, webhookData.payloads[1].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[1].WorkflowRun.HeadSha) + assert.Equal(t, repoName, webhookData.payloads[1].Repo.Name) + assert.Equal(t, "user2/"+repoName, webhookData.payloads[1].Repo.FullName) + } else { + // The first runner pick-up fired in_progress before the run was abandoned. + assert.Len(t, webhookData.payloads, 3) + assert.Equal(t, "in_progress", webhookData.payloads[1].Action) + assert.Equal(t, "in_progress", webhookData.payloads[1].WorkflowRun.Status) + assert.Equal(t, "completed", webhookData.payloads[2].Action) + assert.Equal(t, "completed", webhookData.payloads[2].WorkflowRun.Status) + assert.Equal(t, testRepo.DefaultBranch, webhookData.payloads[2].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[2].WorkflowRun.HeadSha) + assert.Equal(t, repoName, webhookData.payloads[2].Repo.Name) + assert.Equal(t, "user2/"+repoName, webhookData.payloads[2].Repo.FullName) + } } func testWorkflowRunOnStoppingEndlessTasksForMultipleRuns(t *testing.T, webhookData *workflowRunWebhook) { @@ -1741,20 +1772,23 @@ jobs: // 7. validate the webhook is triggered assert.Equal(t, "workflow_run", webhookData.triggeredEvent) - assert.Len(t, webhookData.payloads, 3) - assert.Equal(t, "completed", webhookData.payloads[1].Action) + assert.Len(t, webhookData.payloads, 4) + // payloads[1] is the in_progress event fired when the runner picked up wf1-job + assert.Equal(t, "in_progress", webhookData.payloads[1].Action) + assert.Equal(t, "in_progress", webhookData.payloads[1].WorkflowRun.Status) assert.Equal(t, "push", webhookData.payloads[1].WorkflowRun.Event) + assert.Equal(t, "completed", webhookData.payloads[2].Action) + assert.Equal(t, "push", webhookData.payloads[2].WorkflowRun.Event) - // 3. validate the webhook is triggered - assert.Equal(t, "workflow_run", webhookData.triggeredEvent) - assert.Len(t, webhookData.payloads, 3) - assert.Equal(t, "requested", webhookData.payloads[2].Action) - assert.Equal(t, "queued", webhookData.payloads[2].WorkflowRun.Status) - assert.Equal(t, "workflow_run", webhookData.payloads[2].WorkflowRun.Event) - assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[2].WorkflowRun.HeadBranch) - assert.Equal(t, commitID, webhookData.payloads[2].WorkflowRun.HeadSha) - assert.Equal(t, "repo1", webhookData.payloads[2].Repo.Name) - assert.Equal(t, "user2/repo1", webhookData.payloads[2].Repo.FullName) + // 8. validate the webhook is triggered (requested, wf2 triggered by wf1 completion) + assert.Len(t, webhookData.payloads, 4) + assert.Equal(t, "requested", webhookData.payloads[3].Action) + assert.Equal(t, "queued", webhookData.payloads[3].WorkflowRun.Status) + assert.Equal(t, "workflow_run", webhookData.payloads[3].WorkflowRun.Event) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[3].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[3].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[3].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[3].Repo.FullName) } func testWebhookWorkflowRunDepthLimit(t *testing.T, webhookData *workflowRunWebhook) { From 50a1dc9486fb039a9408fc2ff5efb3bd72629d7e Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 29 Mar 2026 20:48:40 +0200 Subject: [PATCH 141/207] Make task list checkboxes clickable in the preview tab (#37010) When a checkbox is toggled in the markup preview tab, the change is now synced back to the editor textarea. Extracted a `toggleTasklistCheckbox` helper to deduplicate the byte-offset toggle logic. --------- Co-authored-by: Claude (Opus 4.6) --- .../js/features/comp/ComboMarkdownEditor.ts | 15 ++++++++++ web_src/js/markup/tasklist.test.ts | 9 ++++++ web_src/js/markup/tasklist.ts | 30 ++++++++++++------- 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 web_src/js/markup/tasklist.test.ts diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 468f3fc5ca..f16a71a6c5 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -10,6 +10,7 @@ import { } from './EditorUpload.ts'; import {handleGlobalEnterQuickSubmit} from './QuickSubmit.ts'; import {renderPreviewPanelContent} from '../repo-editor.ts'; +import {toggleTasklistCheckbox} from '../../markup/tasklist.ts'; import {easyMDEToolbarActions} from './EasyMDEToolbarActions.ts'; import {initTextExpander} from './TextExpander.ts'; import {showErrorToast} from '../../modules/toast.ts'; @@ -236,6 +237,20 @@ export class ComboMarkdownEditor { const response = await POST(this.previewUrl, {data: formData}); const data = await response.text(); renderPreviewPanelContent(panelPreviewer, data); + // enable task list checkboxes in preview and sync state back to the editor + for (const checkbox of panelPreviewer.querySelectorAll('.task-list-item input[type=checkbox]')) { + checkbox.disabled = false; + checkbox.addEventListener('input', () => { + const position = parseInt(checkbox.getAttribute('data-source-position')!) + 1; + const newContent = toggleTasklistCheckbox(this.value(), position, checkbox.checked); + if (newContent === null) { + checkbox.checked = !checkbox.checked; + return; + } + this.value(newContent); + triggerEditorContentChanged(this.container); + }); + } }); } diff --git a/web_src/js/markup/tasklist.test.ts b/web_src/js/markup/tasklist.test.ts new file mode 100644 index 0000000000..ec5eceebd0 --- /dev/null +++ b/web_src/js/markup/tasklist.test.ts @@ -0,0 +1,9 @@ +import {toggleTasklistCheckbox} from './tasklist.ts'; + +test('toggleTasklistCheckbox', () => { + expect(toggleTasklistCheckbox('- [ ] task', 3, true)).toEqual('- [x] task'); + expect(toggleTasklistCheckbox('- [x] task', 3, false)).toEqual('- [ ] task'); + expect(toggleTasklistCheckbox('- [ ] task', 0, true)).toBeNull(); + expect(toggleTasklistCheckbox('- [ ] task', 99, true)).toBeNull(); + expect(toggleTasklistCheckbox('😀 - [ ] task', 8, true)).toEqual('😀 - [x] task'); +}); diff --git a/web_src/js/markup/tasklist.ts b/web_src/js/markup/tasklist.ts index 7f3417c2bb..557afeaea5 100644 --- a/web_src/js/markup/tasklist.ts +++ b/web_src/js/markup/tasklist.ts @@ -3,6 +3,23 @@ import {showErrorToast} from '../modules/toast.ts'; const preventListener = (e: Event) => e.preventDefault(); +/** + * Toggle a task list checkbox in markdown content. + * `position` is the byte offset of the space or `x` character inside `[ ]`. + * Returns the updated content, or null if the position is invalid. + */ +export function toggleTasklistCheckbox(content: string, position: number, checked: boolean): string | null { + const buffer = new TextEncoder().encode(content); + // Indexes may fall off the ends and return undefined. + if (buffer[position - 1] !== '['.charCodeAt(0) || + buffer[position] !== ' '.charCodeAt(0) && buffer[position] !== 'x'.charCodeAt(0) || + buffer[position + 1] !== ']'.charCodeAt(0)) { + return null; + } + buffer[position] = checked ? 'x'.charCodeAt(0) : ' '.charCodeAt(0); + return new TextDecoder().decode(buffer); +} + /** * Attaches `input` handlers to markdown rendered tasklist checkboxes in comments. * @@ -23,24 +40,17 @@ export function initMarkupTasklist(elMarkup: HTMLElement): void { checkbox.setAttribute('data-editable', 'true'); checkbox.addEventListener('input', async () => { - const checkboxCharacter = checkbox.checked ? 'x' : ' '; const position = parseInt(checkbox.getAttribute('data-source-position')!) + 1; const rawContent = container.querySelector('.raw-content')!; const oldContent = rawContent.textContent; - const encoder = new TextEncoder(); - const buffer = encoder.encode(oldContent); - // Indexes may fall off the ends and return undefined. - if (buffer[position - 1] !== '['.codePointAt(0) || - buffer[position] !== ' '.codePointAt(0) && buffer[position] !== 'x'.codePointAt(0) || - buffer[position + 1] !== ']'.codePointAt(0)) { - // Position is probably wrong. Revert and don't allow change. + const newContent = toggleTasklistCheckbox(oldContent, position, checkbox.checked); + if (newContent === null) { + // Position is probably wrong. Revert and don't allow change. checkbox.checked = !checkbox.checked; throw new Error(`Expected position to be space or x and surrounded by brackets, but it's not: position=${position}`); } - buffer.set(encoder.encode(checkboxCharacter), position); - const newContent = new TextDecoder().decode(buffer); if (newContent === oldContent) { return; From d7070b851389e97fc5ba104f2efe4ec6a2293264 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sun, 29 Mar 2026 17:02:15 -0400 Subject: [PATCH 142/207] Bump go and python versions in nix flake (#37031) --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 6fb3891963..7b9fbb193c 100644 --- a/flake.nix +++ b/flake.nix @@ -33,9 +33,9 @@ inherit (pkgs) lib; # only bump toolchain versions here - go = pkgs.go_1_25; + go = pkgs.go_1_26; nodejs = pkgs.nodejs_24; - python3 = pkgs.python312; + python3 = pkgs.python314; pnpm = pkgs.pnpm_10; # Platform-specific dependencies From cbea04c1fc1af7e9f35303b057dc8f222ac03f08 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:25:18 -0400 Subject: [PATCH 143/207] Update Nix flake (#37024) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 8c7ac0c196..246cfd4e79 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1773821835, - "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", + "lastModified": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", "type": "github" }, "original": { From 2633f9677d1f313b04c30793b4376ff6614109cc Mon Sep 17 00:00:00 2001 From: Myers Carpenter Date: Sun, 29 Mar 2026 20:28:48 -0400 Subject: [PATCH 144/207] Correct swagger annotations for enums, status codes, and notification state (#37030) ## :warning: BREAKING :warning: - delete reaction endpoints is changed to return 204 No Content rather than 200 with no content. ## Summary Add swagger:enum annotations and migrate all enum comments from the deprecated comma-separated format to JSON arrays. Introduce NotifySubjectStateType with open/closed/merged values. Fix delete reaction endpoints to return 204 instead of 200. --- modules/structs/activity.go | 2 +- modules/structs/hook.go | 2 +- modules/structs/issue.go | 26 +-- modules/structs/issue_milestone.go | 3 +- modules/structs/notifications.go | 17 +- modules/structs/org.go | 4 +- modules/structs/org_team.go | 6 +- modules/structs/pull_review.go | 7 +- modules/structs/repo.go | 8 +- modules/structs/repo_collaborator.go | 2 +- modules/structs/repo_file.go | 2 +- routers/api/v1/admin/runners.go | 6 +- routers/api/v1/api.go | 2 - routers/api/v1/org/action.go | 6 +- routers/api/v1/repo/action.go | 8 +- routers/api/v1/repo/issue_reaction.go | 10 +- routers/api/v1/repo/pull.go | 2 + routers/api/v1/user/runners.go | 6 +- services/convert/notification.go | 6 +- services/convert/notification_test.go | 75 +++++++++ services/convert/status.go | 3 + services/forms/repo_form.go | 2 +- templates/swagger/v1_json.tmpl | 166 +++++++++++++------ tests/integration/api_issue_reaction_test.go | 4 +- 24 files changed, 265 insertions(+), 110 deletions(-) diff --git a/modules/structs/activity.go b/modules/structs/activity.go index 9085495593..b896adfed5 100644 --- a/modules/structs/activity.go +++ b/modules/structs/activity.go @@ -12,7 +12,7 @@ type Activity struct { UserID int64 `json:"user_id"` // Receiver user // the type of action // - // enum: create_repo,rename_repo,star_repo,watch_repo,commit_repo,create_issue,create_pull_request,transfer_repo,push_tag,comment_issue,merge_pull_request,close_issue,reopen_issue,close_pull_request,reopen_pull_request,delete_tag,delete_branch,mirror_sync_push,mirror_sync_create,mirror_sync_delete,approve_pull_request,reject_pull_request,comment_pull,publish_release,pull_review_dismissed,pull_request_ready_for_review,auto_merge_pull_request + // enum: ["create_repo","rename_repo","star_repo","watch_repo","commit_repo","create_issue","create_pull_request","transfer_repo","push_tag","comment_issue","merge_pull_request","close_issue","reopen_issue","close_pull_request","reopen_pull_request","delete_tag","delete_branch","mirror_sync_push","mirror_sync_create","mirror_sync_delete","approve_pull_request","reject_pull_request","comment_pull","publish_release","pull_review_dismissed","pull_request_ready_for_review","auto_merge_pull_request"] OpType string `json:"op_type"` // The ID of the user who performed the action ActUserID int64 `json:"act_user_id"` diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 57af38464a..931589696a 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -51,7 +51,7 @@ type CreateHookOptionConfig map[string]string // CreateHookOption options when create a hook type CreateHookOption struct { // required: true - // enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist + // enum: ["dingtalk","discord","gitea","gogs","msteams","slack","telegram","feishu","wechatwork","packagist"] // The type of the webhook to create Type string `json:"type" binding:"Required"` // required: true diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 2540481d0f..1efe3334ca 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -14,6 +14,8 @@ import ( ) // StateType issue state type +// +// swagger:enum StateType type StateType string const ( @@ -21,10 +23,11 @@ const ( StateOpen StateType = "open" // StateClosed pr is closed StateClosed StateType = "closed" - // StateAll is all - StateAll StateType = "all" ) +// StateAll is a query parameter filter value, not a valid object state. +const StateAll = "all" + // PullRequestMeta PR info if an issue is a PR type PullRequestMeta struct { HasMerged bool `json:"merged"` @@ -58,15 +61,11 @@ type Issue struct { Labels []*Label `json:"labels"` Milestone *Milestone `json:"milestone"` // deprecated - Assignee *User `json:"assignee"` - Assignees []*User `json:"assignees"` - // Whether the issue is open or closed - // - // type: string - // enum: open,closed - State StateType `json:"state"` - IsLocked bool `json:"is_locked"` - Comments int `json:"comments"` + Assignee *User `json:"assignee"` + Assignees []*User `json:"assignees"` + State StateType `json:"state"` + IsLocked bool `json:"is_locked"` + Comments int `json:"comments"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time @@ -132,6 +131,8 @@ type IssueDeadline struct { } // IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes" +// +// swagger:enum IssueFormFieldType type IssueFormFieldType string const ( @@ -168,7 +169,8 @@ func (iff IssueFormField) VisibleInContent() bool { } // IssueFormFieldVisible defines issue form field visible -// swagger:model +// +// swagger:enum IssueFormFieldVisible type IssueFormFieldVisible string const ( diff --git a/modules/structs/issue_milestone.go b/modules/structs/issue_milestone.go index 226c613d47..dd8bdc6cda 100644 --- a/modules/structs/issue_milestone.go +++ b/modules/structs/issue_milestone.go @@ -40,7 +40,7 @@ type CreateMilestoneOption struct { // swagger:strfmt date-time // Deadline is the due date for the milestone Deadline *time.Time `json:"due_on"` - // enum: open,closed + // enum: ["open","closed"] // State indicates the initial state of the milestone State string `json:"state"` } @@ -52,6 +52,7 @@ type EditMilestoneOption struct { // Description provides updated details about the milestone Description *string `json:"description"` // State indicates the updated state of the milestone + // enum: ["open","closed"] State *string `json:"state"` // Deadline is the updated due date for the milestone Deadline *time.Time `json:"due_on"` diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go index cee5da6624..d7aa0783dc 100644 --- a/modules/structs/notifications.go +++ b/modules/structs/notifications.go @@ -40,7 +40,7 @@ type NotificationSubject struct { // Type indicates the type of the notification subject Type NotifySubjectType `json:"type" binding:"In(Issue,Pull,Commit,Repository)"` // State indicates the current state of the notification subject - State StateType `json:"state"` + State NotifySubjectStateType `json:"state"` } // NotificationCount number of unread notifications @@ -49,7 +49,22 @@ type NotificationCount struct { New int64 `json:"new"` } +// NotifySubjectStateType represents the state of a notification subject +// swagger:enum NotifySubjectStateType +type NotifySubjectStateType string + +const ( + // NotifySubjectStateOpen is an open subject + NotifySubjectStateOpen NotifySubjectStateType = "open" + // NotifySubjectStateClosed is a closed subject + NotifySubjectStateClosed NotifySubjectStateType = "closed" + // NotifySubjectStateMerged is a merged pull request + NotifySubjectStateMerged NotifySubjectStateType = "merged" +) + // NotifySubjectType represent type of notification subject +// +// swagger:enum NotifySubjectType type NotifySubjectType string const ( diff --git a/modules/structs/org.go b/modules/structs/org.go index d79b1d1d1c..723689cb53 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -60,7 +60,7 @@ type CreateOrgOption struct { // The location of the organization Location string `json:"location" binding:"MaxSize(50)"` // possible values are `public` (default), `limited` or `private` - // enum: public,limited,private + // enum: ["public","limited","private"] Visibility string `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` @@ -79,7 +79,7 @@ type EditOrgOption struct { // The location of the organization Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` - // enum: public,limited,private + // enum: ["public","limited","private"] Visibility *string `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index d34de5b6d2..f730a5681c 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -16,7 +16,7 @@ type Team struct { Organization *Organization `json:"organization"` // Whether the team has access to all repositories in the organization IncludesAllRepositories bool `json:"includes_all_repositories"` - // enum: none,read,write,admin,owner + // enum: ["none","read","write","admin","owner"] Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. @@ -35,7 +35,7 @@ type CreateTeamOption struct { Description string `json:"description" binding:"MaxSize(255)"` // Whether the team has access to all repositories in the organization IncludesAllRepositories bool `json:"includes_all_repositories"` - // enum: read,write,admin + // enum: ["read","write","admin"] Permission string `json:"permission"` // example: ["repo.actions","repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.ext_wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. @@ -54,7 +54,7 @@ type EditTeamOption struct { Description *string `json:"description" binding:"MaxSize(255)"` // Whether the team has access to all repositories in the organization IncludesAllRepositories *bool `json:"includes_all_repositories"` - // enum: read,write,admin + // enum: ["read","write","admin"] Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. diff --git a/modules/structs/pull_review.go b/modules/structs/pull_review.go index f44d2f84f5..de0677efab 100644 --- a/modules/structs/pull_review.go +++ b/modules/structs/pull_review.go @@ -8,6 +8,8 @@ import ( ) // ReviewStateType review state type +// +// swagger:enum ReviewStateType type ReviewStateType string const ( @@ -21,10 +23,11 @@ const ( ReviewStateRequestChanges ReviewStateType = "REQUEST_CHANGES" // ReviewStateRequestReview review is requested from user ReviewStateRequestReview ReviewStateType = "REQUEST_REVIEW" - // ReviewStateUnknown state of pr is unknown - ReviewStateUnknown ReviewStateType = "" ) +// ReviewStateUnknown is an internal sentinel for unknown review state, not a valid API value. +const ReviewStateUnknown = "" + // PullReview represents a pull request review type PullReview struct { ID int64 `json:"id"` diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 3507cc410a..7cd64fd7a4 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -114,7 +114,7 @@ type Repository struct { Internal bool `json:"internal"` MirrorInterval string `json:"mirror_interval"` // ObjectFormatName of the underlying git repository - // enum: sha1,sha256 + // enum: ["sha1","sha256"] ObjectFormatName string `json:"object_format_name"` // swagger:strfmt date-time MirrorUpdated time.Time `json:"mirror_updated"` @@ -150,10 +150,10 @@ type CreateRepoOption struct { // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` // TrustModel of the repository - // enum: default,collaborator,committer,collaboratorcommitter + // enum: ["default","collaborator","committer","collaboratorcommitter"] TrustModel string `json:"trust_model"` // ObjectFormatName of the underlying git repository, empty string for default (sha1) - // enum: sha1,sha256 + // enum: ["sha1","sha256"] ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"` } @@ -378,7 +378,7 @@ type MigrateRepoOptions struct { // required: true RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` - // enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase,codecommit + // enum: ["git","github","gitea","gitlab","gogs","onedev","gitbucket","codebase","codecommit"] Service string `json:"service"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` diff --git a/modules/structs/repo_collaborator.go b/modules/structs/repo_collaborator.go index 9ede7f075a..6b315df403 100644 --- a/modules/structs/repo_collaborator.go +++ b/modules/structs/repo_collaborator.go @@ -5,7 +5,7 @@ package structs // AddCollaboratorOption options when adding a user as a collaborator of a repository type AddCollaboratorOption struct { - // enum: read,write,admin + // enum: ["read","write","admin"] // Permission level to grant the collaborator Permission *string `json:"permission"` } diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 59665062b7..53ce5aeae2 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -72,7 +72,7 @@ type ChangeFileOperation struct { // indicates what to do with the file: "create" for creating a new file, "update" for updating an existing file, // "upload" for creating or updating a file, "rename" for renaming a file, and "delete" for deleting an existing file. // required: true - // enum: create,update,upload,rename,delete + // enum: ["create","update","upload","rename","delete"] Operation string `json:"operation" binding:"Required"` // path to the existing or new file // required: true diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 93983f6c7e..3d27c87935 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -40,7 +40,7 @@ func ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -63,7 +63,7 @@ func GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -115,7 +115,7 @@ func UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ea595407d1..e1d836b5c8 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -11,11 +11,9 @@ // // Consumes: // - application/json -// - text/plain // // Produces: // - application/json -// - text/html // // Security: // - BasicAuth : diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 18ed602ddb..01b57b3fac 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -492,7 +492,7 @@ func (Action) ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -520,7 +520,7 @@ func (Action) GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -582,7 +582,7 @@ func (Action) UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 0c48f732ab..7ac8a10575 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -561,7 +561,7 @@ func (Action) ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -594,7 +594,7 @@ func (Action) GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -666,7 +666,7 @@ func (Action) UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -1192,7 +1192,7 @@ func GetWorkflowRun(ctx *context.APIContext) { // - name: run // in: path // description: id of the run - // type: string + // type: integer // required: true // responses: // "200": diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index e535b5e009..1f313acde8 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -175,7 +175,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/EditReactionOption" // responses: - // "200": + // "204": // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" @@ -248,8 +248,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp ctx.APIErrorInternal(err) return } - // ToDo respond 204 - ctx.Status(http.StatusOK) + ctx.Status(http.StatusNoContent) } } @@ -408,7 +407,7 @@ func DeleteIssueReaction(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/EditReactionOption" // responses: - // "200": + // "204": // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" @@ -464,7 +463,6 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i ctx.APIErrorInternal(err) return } - // ToDo respond 204 - ctx.Status(http.StatusOK) + ctx.Status(http.StatusNoContent) } } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index f405a3152f..a045bba49c 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -898,6 +898,8 @@ func MergePullRequest(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" // "405": diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go index 667bdb36fe..e06b022f35 100644 --- a/routers/api/v1/user/runners.go +++ b/routers/api/v1/user/runners.go @@ -40,7 +40,7 @@ func ListRunners(ctx *context.APIContext) { // required: false // responses: // "200": - // "$ref": "#/definitions/ActionRunnersResponse" + // "$ref": "#/responses/RunnerList" // "400": // "$ref": "#/responses/error" // "404": @@ -63,7 +63,7 @@ func GetRunner(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": @@ -115,7 +115,7 @@ func UpdateRunner(ctx *context.APIContext) { // "$ref": "#/definitions/EditActionRunnerOption" // responses: // "200": - // "$ref": "#/definitions/ActionRunner" + // "$ref": "#/responses/Runner" // "400": // "$ref": "#/responses/error" // "404": diff --git a/services/convert/notification.go b/services/convert/notification.go index e91bc7dcde..3a1ae09dc5 100644 --- a/services/convert/notification.go +++ b/services/convert/notification.go @@ -47,7 +47,7 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification) result.Subject.Title = n.Issue.Title result.Subject.URL = n.Issue.APIURL(ctx) result.Subject.HTMLURL = n.Issue.HTMLURL(ctx) - result.Subject.State = n.Issue.State() + result.Subject.State = api.NotifySubjectStateType(n.Issue.State()) comment, err := n.Issue.GetLastComment(ctx) if err == nil && comment != nil { result.Subject.LatestCommentURL = comment.APIURL(ctx) @@ -60,7 +60,7 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification) result.Subject.Title = n.Issue.Title result.Subject.URL = n.Issue.APIURL(ctx) result.Subject.HTMLURL = n.Issue.HTMLURL(ctx) - result.Subject.State = n.Issue.State() + result.Subject.State = api.NotifySubjectStateType(n.Issue.State()) comment, err := n.Issue.GetLastComment(ctx) if err == nil && comment != nil { result.Subject.LatestCommentURL = comment.APIURL(ctx) @@ -70,7 +70,7 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification) if err := n.Issue.LoadPullRequest(ctx); err == nil && n.Issue.PullRequest != nil && n.Issue.PullRequest.HasMerged { - result.Subject.State = "merged" + result.Subject.State = api.NotifySubjectStateMerged } } case activities_model.NotificationSourceCommit: diff --git a/services/convert/notification_test.go b/services/convert/notification_test.go index 718a070819..0a4f9d6c0a 100644 --- a/services/convert/notification_test.go +++ b/services/convert/notification_test.go @@ -7,12 +7,15 @@ import ( "testing" activities_model "code.gitea.io/gitea/models/activities" + issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestToNotificationThreadIncludesRepoForAccessibleUser(t *testing.T) { @@ -36,6 +39,78 @@ func TestToNotificationThreadOmitsRepoWhenAccessRevoked(t *testing.T) { assert.Nil(t, thread.Repository) } +func TestToNotificationThread(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("issue notification", func(t *testing.T) { + // Notification 1: source=issue, issue_id=1, status=unread + n := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 1}) + require.NoError(t, n.LoadAttributes(t.Context())) + + thread := ToNotificationThread(t.Context(), n) + assert.Equal(t, int64(1), thread.ID) + assert.True(t, thread.Unread) + assert.False(t, thread.Pinned) + require.NotNil(t, thread.Subject) + assert.Equal(t, api.NotifySubjectIssue, thread.Subject.Type) + assert.Equal(t, api.NotifySubjectStateOpen, thread.Subject.State) + }) + + t.Run("pinned notification", func(t *testing.T) { + // Notification 3: status=pinned + n := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 3}) + require.NoError(t, n.LoadAttributes(t.Context())) + + thread := ToNotificationThread(t.Context(), n) + assert.False(t, thread.Unread) + assert.True(t, thread.Pinned) + }) + + t.Run("merged pull request returns merged state", func(t *testing.T) { + // Issue 2 is a pull request; pull_request 1 has has_merged=true. + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + + n := &activities_model.Notification{ + ID: 999, + UserID: 2, + RepoID: repo.ID, + Status: activities_model.NotificationStatusUnread, + Source: activities_model.NotificationSourcePullRequest, + IssueID: issue.ID, + Issue: issue, + Repository: repo, + } + + thread := ToNotificationThread(t.Context(), n) + require.NotNil(t, thread.Subject) + assert.Equal(t, api.NotifySubjectPull, thread.Subject.Type) + assert.Equal(t, api.NotifySubjectStateMerged, thread.Subject.State) + }) + + t.Run("open pull request returns open state", func(t *testing.T) { + // Issue 3 is a pull request; pull_request 2 has has_merged=false. + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + + n := &activities_model.Notification{ + ID: 998, + UserID: 2, + RepoID: repo.ID, + Status: activities_model.NotificationStatusUnread, + Source: activities_model.NotificationSourcePullRequest, + IssueID: issue.ID, + Issue: issue, + Repository: repo, + } + + thread := ToNotificationThread(t.Context(), n) + require.NotNil(t, thread.Subject) + assert.Equal(t, api.NotifySubjectPull, thread.Subject.Type) + assert.Equal(t, api.NotifySubjectStateOpen, thread.Subject.State) + }) +} + func newRepoNotification(t *testing.T, repoID, userID int64) *activities_model.Notification { t.Helper() diff --git a/services/convert/status.go b/services/convert/status.go index fe8240a8f7..a8ef94d107 100644 --- a/services/convert/status.go +++ b/services/convert/status.go @@ -9,6 +9,7 @@ import ( git_model "code.gitea.io/gitea/models/git" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/commitstatus" api "code.gitea.io/gitea/modules/structs" ) @@ -55,6 +56,8 @@ func ToCombinedStatus(ctx context.Context, commitID string, statuses []*git_mode if combinedStatus != nil { status.Statuses = ToCommitStatuses(ctx, statuses) status.State = combinedStatus.State + } else { + status.State = commitstatus.CommitStatusPending } return &status } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 8b69c6bcc6..7ccf0aa622 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -521,7 +521,7 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors) // swagger:model MergePullRequestOption type MergePullRequestForm struct { // required: true - // enum: merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged + // enum: ["merge","rebase","rebase-merge","squash","fast-forward-only","manually-merged"] Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"` MergeTitleField string MergeMessageField string diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index adc6c18175..e01ff1112b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1,11 +1,9 @@ { "consumes": [ - "application/json", - "text/plain" + "application/json" ], "produces": [ - "application/json", - "text/html" + "application/json" ], "schemes": [ "https", @@ -86,7 +84,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunnersResponse" + "$ref": "#/responses/RunnerList" }, "400": { "$ref": "#/responses/error" @@ -135,7 +133,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -205,7 +203,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -2008,7 +2006,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunnersResponse" + "$ref": "#/responses/RunnerList" }, "400": { "$ref": "#/responses/error" @@ -2073,7 +2071,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -2157,7 +2155,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -4989,7 +4987,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunnersResponse" + "$ref": "#/responses/RunnerList" }, "400": { "$ref": "#/responses/error" @@ -5068,7 +5066,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -5166,7 +5164,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -5287,7 +5285,7 @@ "required": true }, { - "type": "string", + "type": "integer", "description": "id of the run", "name": "run", "in": "path", @@ -10230,7 +10228,7 @@ } ], "responses": { - "200": { + "204": { "$ref": "#/responses/empty" }, "403": { @@ -11969,7 +11967,7 @@ } ], "responses": { - "200": { + "204": { "$ref": "#/responses/empty" }, "403": { @@ -14495,6 +14493,9 @@ "200": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, @@ -18670,7 +18671,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunnersResponse" + "$ref": "#/responses/RunnerList" }, "400": { "$ref": "#/responses/error" @@ -18719,7 +18720,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -18789,7 +18790,7 @@ ], "responses": { "200": { - "$ref": "#/definitions/ActionRunner" + "$ref": "#/responses/Runner" }, "400": { "$ref": "#/responses/error" @@ -23887,7 +23888,16 @@ "x-go-name": "CommitID" }, "event": { - "$ref": "#/definitions/ReviewStateType" + "type": "string", + "enum": [ + "APPROVED", + "PENDING", + "COMMENT", + "REQUEST_CHANGES", + "REQUEST_REVIEW" + ], + "x-go-enum-desc": "APPROVED ReviewStateApproved ReviewStateApproved pr is approved\nPENDING ReviewStatePending ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview ReviewStateRequestReview review is requested from user", + "x-go-name": "Event" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -24835,6 +24845,10 @@ "state": { "description": "State indicates the updated state of the milestone", "type": "string", + "enum": [ + "open", + "closed" + ], "x-go-name": "State" }, "title": { @@ -26272,7 +26286,13 @@ "$ref": "#/definitions/RepositoryMeta" }, "state": { - "$ref": "#/definitions/StateType" + "type": "string", + "enum": [ + "open", + "closed" + ], + "x-go-enum-desc": "open StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "x-go-name": "State" }, "time_estimate": { "type": "integer", @@ -26373,7 +26393,16 @@ "x-go-name": "ID" }, "type": { - "$ref": "#/definitions/IssueFormFieldType" + "type": "string", + "enum": [ + "markdown", + "textarea", + "input", + "dropdown", + "checkboxes" + ], + "x-go-enum-desc": "markdown IssueFormFieldTypeMarkdown\ntextarea IssueFormFieldTypeTextarea\ninput IssueFormFieldTypeInput\ndropdown IssueFormFieldTypeDropdown\ncheckboxes IssueFormFieldTypeCheckboxes", + "x-go-name": "Type" }, "validations": { "type": "object", @@ -26383,23 +26412,18 @@ "visible": { "type": "array", "items": { - "$ref": "#/definitions/IssueFormFieldVisible" + "type": "string", + "enum": [ + "form", + "content" + ], + "x-go-enum-desc": "form IssueFormFieldVisibleForm\ncontent IssueFormFieldVisibleContent" }, "x-go-name": "Visible" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, - "IssueFormFieldType": { - "type": "string", - "title": "IssueFormFieldType defines issue form field type, can be \"markdown\", \"textarea\", \"input\", \"dropdown\" or \"checkboxes\"", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, - "IssueFormFieldVisible": { - "description": "IssueFormFieldVisible defines issue form field visible", - "type": "string", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "IssueLabelsOption": { "description": "IssueLabelsOption a collection of labels", "type": "object", @@ -26897,7 +26921,14 @@ "x-go-name": "OpenIssues" }, "state": { - "$ref": "#/definitions/StateType" + "description": "State indicates if the milestone is open or closed\nopen StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "type": "string", + "enum": [ + "open", + "closed" + ], + "x-go-enum-desc": "open StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "x-go-name": "State" }, "title": { "description": "Title is the title of the milestone", @@ -27111,7 +27142,15 @@ "x-go-name": "LatestCommentURL" }, "state": { - "$ref": "#/definitions/StateType" + "description": "State indicates the current state of the notification subject\nopen NotifySubjectStateOpen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged NotifySubjectStateMerged is a merged pull request", + "type": "string", + "enum": [ + "open", + "closed", + "merged" + ], + "x-go-enum-desc": "open NotifySubjectStateOpen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged NotifySubjectStateMerged is a merged pull request", + "x-go-name": "State" }, "title": { "description": "Title is the title of the notification subject", @@ -27119,7 +27158,16 @@ "x-go-name": "Title" }, "type": { - "$ref": "#/definitions/NotifySubjectType" + "description": "Type indicates the type of the notification subject\nIssue NotifySubjectIssue NotifySubjectIssue an issue is subject of an notification\nPull NotifySubjectPull NotifySubjectPull an pull is subject of an notification\nCommit NotifySubjectCommit NotifySubjectCommit an commit is subject of an notification\nRepository NotifySubjectRepository NotifySubjectRepository an repository is subject of an notification", + "type": "string", + "enum": [ + "Issue", + "Pull", + "Commit", + "Repository" + ], + "x-go-enum-desc": "Issue NotifySubjectIssue NotifySubjectIssue an issue is subject of an notification\nPull NotifySubjectPull NotifySubjectPull an pull is subject of an notification\nCommit NotifySubjectCommit NotifySubjectCommit an commit is subject of an notification\nRepository NotifySubjectRepository NotifySubjectRepository an repository is subject of an notification", + "x-go-name": "Type" }, "url": { "description": "URL is the API URL for the notification subject", @@ -27169,11 +27217,6 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, - "NotifySubjectType": { - "description": "NotifySubjectType represent type of notification subject", - "type": "string", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "OAuth2Application": { "type": "object", "title": "OAuth2Application represents an OAuth2 application.", @@ -27806,7 +27849,14 @@ "x-go-name": "ReviewComments" }, "state": { - "$ref": "#/definitions/StateType" + "description": "The current state of the pull request\nopen StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "type": "string", + "enum": [ + "open", + "closed" + ], + "x-go-enum-desc": "open StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "x-go-name": "State" }, "title": { "description": "The title of the pull request", @@ -27898,7 +27948,16 @@ "x-go-name": "Stale" }, "state": { - "$ref": "#/definitions/ReviewStateType" + "type": "string", + "enum": [ + "APPROVED", + "PENDING", + "COMMENT", + "REQUEST_CHANGES", + "REQUEST_REVIEW" + ], + "x-go-enum-desc": "APPROVED ReviewStateApproved ReviewStateApproved pr is approved\nPENDING ReviewStatePending ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview ReviewStateRequestReview review is requested from user", + "x-go-name": "State" }, "submitted_at": { "type": "string", @@ -28635,11 +28694,6 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, - "ReviewStateType": { - "description": "ReviewStateType review state type", - "type": "string", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "RunDetails": { "description": "RunDetails returns workflow_dispatch runid and url", "type": "object", @@ -28714,11 +28768,6 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, - "StateType": { - "description": "StateType issue state type", - "type": "string", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "StopWatch": { "description": "StopWatch represent a running stopwatch", "type": "object", @@ -28772,7 +28821,16 @@ "x-go-name": "Body" }, "event": { - "$ref": "#/definitions/ReviewStateType" + "type": "string", + "enum": [ + "APPROVED", + "PENDING", + "COMMENT", + "REQUEST_CHANGES", + "REQUEST_REVIEW" + ], + "x-go-enum-desc": "APPROVED ReviewStateApproved ReviewStateApproved pr is approved\nPENDING ReviewStatePending ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview ReviewStateRequestReview review is requested from user", + "x-go-name": "Event" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go index 01588f9900..d099e72edb 100644 --- a/tests/integration/api_issue_reaction_test.go +++ b/tests/integration/api_issue_reaction_test.go @@ -44,7 +44,7 @@ func TestAPIIssuesReactions(t *testing.T) { req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{ Reaction: "zzz", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) + MakeRequest(t, req, http.StatusNoContent) // Add allowed reaction req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ @@ -111,7 +111,7 @@ func TestAPICommentReactions(t *testing.T) { req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{ Reaction: "eyes", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) + MakeRequest(t, req, http.StatusNoContent) t.Run("UnrelatedCommentID", func(t *testing.T) { // Using the ID of a comment that does not belong to the repository must fail From c31e0cfc1c150abc9d1f361c16ca6b5ea0b7b97e Mon Sep 17 00:00:00 2001 From: Myers Carpenter Date: Mon, 30 Mar 2026 09:44:32 -0400 Subject: [PATCH 145/207] Expose content_version for optimistic locking on issue and PR edits (#37035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `content_version` field to Issue and PullRequest API responses - Accept optional `content_version` in `PATCH /repos/{owner}/{repo}/issues/{index}` and `PATCH /repos/{owner}/{repo}/pulls/{index}` — returns 409 Conflict when stale, succeeds silently when omitted (backward compatible) - Pre-check `content_version` before any mutations to prevent partial writes (e.g. title updated but body rejected) Co-authored-by: wxiaoguang --- modules/structs/issue.go | 4 +- modules/structs/pull.go | 4 +- routers/api/v1/repo/issue.go | 20 ++++++- routers/api/v1/repo/pull.go | 17 +++++- services/convert/issue.go | 3 +- services/convert/pull.go | 2 + templates/swagger/v1_json.tmpl | 21 +++++++ tests/integration/api_issue_test.go | 89 +++++++++++++++++++++++------ 8 files changed, 135 insertions(+), 25 deletions(-) diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 1efe3334ca..a34e4b0693 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -80,7 +80,8 @@ type Issue struct { PullRequest *PullRequestMeta `json:"pull_request"` Repo *RepositoryMeta `json:"repository"` - PinOrder int `json:"pin_order"` + PinOrder int `json:"pin_order"` + ContentVersion int `json:"content_version"` } // CreateIssueOption options to create one issue @@ -114,6 +115,7 @@ type EditIssueOption struct { // swagger:strfmt date-time Deadline *time.Time `json:"due_date"` RemoveDeadline *bool `json:"unset_due_date"` + ContentVersion *int `json:"content_version"` } // EditDeadlineOption options for creating a deadline diff --git a/modules/structs/pull.go b/modules/structs/pull.go index 3ad2f78bd3..ad320e2b82 100644 --- a/modules/structs/pull.go +++ b/modules/structs/pull.go @@ -90,7 +90,8 @@ type PullRequest struct { Closed *time.Time `json:"closed_at"` // The pin order for the pull request - PinOrder int `json:"pin_order"` + PinOrder int `json:"pin_order"` + ContentVersion int `json:"content_version"` } // PRBranchInfo information about a branch @@ -168,6 +169,7 @@ type EditPullRequestOption struct { RemoveDeadline *bool `json:"unset_due_date"` // Whether to allow maintainer edits AllowMaintainerEdit *bool `json:"allow_maintainer_edit"` + ContentVersion *int `json:"content_version"` } // ChangedFile store information about files affected by the pull request diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index db205380e4..20ccd099a4 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -726,6 +726,9 @@ func EditIssue(ctx *context.APIContext) { // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue // --- // summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored. + // description: | + // Pass `content_version` to enable optimistic locking on body edits. + // If the version doesn't match the current value, the request fails with 409 Conflict. // consumes: // - application/json // produces: @@ -785,6 +788,15 @@ func EditIssue(ctx *context.APIContext) { return } + // Fail fast: if content_version is provided and already stale, reject + // before any mutations. The DB-level check in ChangeContent still + // handles concurrent requests. + // TODO: wrap all mutations in a transaction to fully prevent partial writes. + if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + return + } + if len(form.Title) > 0 { err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title) if err != nil { @@ -793,10 +805,14 @@ func EditIssue(ctx *context.APIContext) { } } if form.Body != nil { - err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion) + contentVersion := issue.ContentVersion + if form.ContentVersion != nil { + contentVersion = *form.ContentVersion + } + err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusConflict, err) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index a045bba49c..ef86f413b7 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -657,6 +657,15 @@ func EditPullRequest(ctx *context.APIContext) { return } + // Fail fast: if content_version is provided and already stale, reject + // before any mutations. The DB-level check in ChangeContent still + // handles concurrent requests. + // TODO: wrap all mutations in a transaction to fully prevent partial writes. + if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + return + } + if len(form.Title) > 0 { err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title) if err != nil { @@ -665,10 +674,14 @@ func EditPullRequest(ctx *context.APIContext) { } } if form.Body != nil { - err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion) + contentVersion := issue.ContentVersion + if form.ContentVersion != nil { + contentVersion = *form.ContentVersion + } + err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusConflict, err) return } diff --git a/services/convert/issue.go b/services/convert/issue.go index acd67fece4..61f11d8f19 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -62,7 +62,8 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss Updated: issue.UpdatedUnix.AsTime(), PinOrder: util.Iif(issue.PinOrder == -1, 0, issue.PinOrder), // -1 means loaded with no pin order - TimeEstimate: issue.TimeEstimate, + TimeEstimate: issue.TimeEstimate, + ContentVersion: issue.ContentVersion, } if issue.Repo != nil { diff --git a/services/convert/pull.go b/services/convert/pull.go index bb675811f2..5c7c99f2ce 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -97,6 +97,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u Created: pr.Issue.CreatedUnix.AsTimePtr(), Updated: pr.Issue.UpdatedUnix.AsTimePtr(), PinOrder: util.Iif(apiIssue.PinOrder == -1, 0, apiIssue.PinOrder), + ContentVersion: apiIssue.ContentVersion, // output "[]" rather than null to align to github outputs RequestedReviewers: []*api.User{}, @@ -372,6 +373,7 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs Created: pr.Issue.CreatedUnix.AsTimePtr(), Updated: pr.Issue.UpdatedUnix.AsTimePtr(), PinOrder: util.Iif(apiIssue.PinOrder == -1, 0, apiIssue.PinOrder), + ContentVersion: apiIssue.ContentVersion, AllowMaintainerEdit: pr.AllowMaintainerEdit, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e01ff1112b..5ae0f197df 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -10362,6 +10362,7 @@ } }, "patch": { + "description": "Pass `content_version` to enable optimistic locking on body edits.\nIf the version doesn't match the current value, the request fails with 409 Conflict.\n", "consumes": [ "application/json" ], @@ -24766,6 +24767,11 @@ "type": "string", "x-go-name": "Body" }, + "content_version": { + "type": "integer", + "format": "int64", + "x-go-name": "ContentVersion" + }, "due_date": { "type": "string", "format": "date-time", @@ -24938,6 +24944,11 @@ "type": "string", "x-go-name": "Body" }, + "content_version": { + "type": "integer", + "format": "int64", + "x-go-name": "ContentVersion" + }, "due_date": { "type": "string", "format": "date-time", @@ -26223,6 +26234,11 @@ "format": "int64", "x-go-name": "Comments" }, + "content_version": { + "type": "integer", + "format": "int64", + "x-go-name": "ContentVersion" + }, "created_at": { "type": "string", "format": "date-time", @@ -27725,6 +27741,11 @@ "format": "int64", "x-go-name": "Comments" }, + "content_version": { + "type": "integer", + "format": "int64", + "x-go-name": "ContentVersion" + }, "created_at": { "type": "string", "format": "date-time", diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 8d85543dc8..c3e96059de 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -25,9 +25,19 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAPIListIssues(t *testing.T) { +func TestAPIIssue(t *testing.T) { defer tests.PrepareTestEnv(t)() + t.Run("ListIssues", testAPIListIssues) + t.Run("ListIssuesPublicOnly", testAPIListIssuesPublicOnly) + t.Run("SearchIssues", testAPISearchIssues) + t.Run("SearchIssuesWithLabels", testAPISearchIssuesWithLabels) + t.Run("EditIssue", testAPIEditIssue) + t.Run("IssueContentVersion", testAPIIssueContentVersion) + t.Run("CreateIssue", testAPICreateIssue) + t.Run("CreateIssueParallel", testAPICreateIssueParallel) +} +func testAPIListIssues(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -75,9 +85,7 @@ func TestAPIListIssues(t *testing.T) { } } -func TestAPIListIssuesPublicOnly(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIListIssuesPublicOnly(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID}) @@ -103,8 +111,7 @@ func TestAPIListIssuesPublicOnly(t *testing.T) { MakeRequest(t, req, http.StatusForbidden) } -func TestAPICreateIssue(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testAPICreateIssue(t *testing.T) { const body, title = "apiTestBody", "apiTestTitle" repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -142,9 +149,7 @@ func TestAPICreateIssue(t *testing.T) { MakeRequest(t, req, http.StatusForbidden) } -func TestAPICreateIssueParallel(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPICreateIssueParallel(t *testing.T) { // FIXME: There seems to be a bug in github.com/mattn/go-sqlite3 with sqlite_unlock_notify, when doing concurrent writes to the same database, // some requests may get stuck in "go-sqlite3.(*SQLiteRows).Next", "go-sqlite3.(*SQLiteStmt).exec" and "go-sqlite3.unlock_notify_wait", // because the "unlock_notify_wait" never returns and the internal lock never gets releases. @@ -152,7 +157,7 @@ func TestAPICreateIssueParallel(t *testing.T) { // The trigger is: a previous test created issues and made the real issue indexer queue start processing, then this test does concurrent writing. // Adding this "Sleep" makes go-sqlite3 "finish" some internal operations before concurrent writes and then won't get stuck. // To reproduce: make a new test run these 2 tests enough times: - // > func TestBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } } + // > func testBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } } // Usually the test gets stuck in fewer than 10 iterations without this "sleep". time.Sleep(time.Second) @@ -197,9 +202,7 @@ func TestAPICreateIssueParallel(t *testing.T) { wg.Wait() } -func TestAPIEditIssue(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIEditIssue(t *testing.T) { issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}) repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}) @@ -263,8 +266,7 @@ func TestAPIEditIssue(t *testing.T) { assert.Equal(t, title, issueAfter.Title) } -func TestAPISearchIssues(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testAPISearchIssues(t *testing.T) { defer test.MockVariableValue(&setting.API.DefaultPagingNum, 20)() expectedIssueCount := 20 // 20 is from the fixtures @@ -391,9 +393,7 @@ func TestAPISearchIssues(t *testing.T) { assert.Len(t, apiIssues, 3) } -func TestAPISearchIssuesWithLabels(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPISearchIssuesWithLabels(t *testing.T) { // as this API was used in the frontend, it uses UI page size expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures @@ -448,3 +448,56 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) } + +func testAPIIssueContentVersion(t *testing.T) { + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index) + + t.Run("ResponseIncludesContentVersion", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", urlStr).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + apiIssue := DecodeJSON(t, resp, &api.Issue{}) + assert.GreaterOrEqual(t, apiIssue.ContentVersion, 0) + }) + + t.Run("EditWithCorrectVersion", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", urlStr).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var before api.Issue + DecodeJSON(t, resp, &before) + req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{ + Body: new("updated body with correct version"), + ContentVersion: new(before.ContentVersion), + }).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusCreated) + after := DecodeJSON(t, resp, &api.Issue{}) + assert.Equal(t, "updated body with correct version", after.Body) + assert.Greater(t, after.ContentVersion, before.ContentVersion) + }) + + t.Run("EditWithWrongVersion", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{ + Body: new("should fail"), + ContentVersion: new(99999), + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusConflict) + }) + + t.Run("EditWithoutVersion", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{ + Body: new("edit without version succeeds"), + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + }) +} From 539654831a1cfc4bab38d71810f25bdac4437164 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Mon, 30 Mar 2026 09:47:41 -0400 Subject: [PATCH 146/207] bump snapcraft deps (#37039) --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 4f3c4e9ff4..a1112def0c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -44,7 +44,7 @@ parts: source: . stage-packages: [ git, sqlite3, openssh-client ] build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential] - build-snaps: [ go/1.25/stable, node/22/stable ] + build-snaps: [ go/1.26/stable, node/24/stable ] build-environment: - LDFLAGS: "" override-pull: | From 612ce46cda56dee5c922e5775bd3250474ab5b42 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 30 Mar 2026 16:59:10 +0200 Subject: [PATCH 147/207] Fix theme discovery and Vite dev server in dev mode (#37033) 1. In dev mode, discover themes from source files in `web_src/css/themes/` instead of AssetFS. In prod, use AssetFS only. Extract shared `collectThemeFiles` helper to deduplicate theme file handling. 2. Implement `fs.ReadDirFS` on `LayeredFS` to support theme file discovery. 3. `IsViteDevMode` now performs an HTTP health check against the vite dev server instead of only checking the port file exists. Result is cached with a 1-second TTL. 4. Refactor theme caching from mutex to atomic pointer with time-based invalidation, allowing themes to refresh when vite dev mode state changes. 5. Move `ViteDevMiddleware` into `ProtocolMiddlewares` so it applies to both install and web routes. 6. Show a `ViteDevMode` label in the page footer when vite dev server is active. 7. Add `/__vite_dev_server_check` endpoint to vite dev server for the health check. 8. Ensure `.vite` directory exists before writing the dev-port file. 9. Minor CSS fixes: footer gap, navbar mobile alignment. --- This PR was written with the help of Claude Opus 4.6 --------- Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- modules/assetfs/layered.go | 25 +++++++ modules/public/vitedev.go | 67 +++++++++++++----- modules/web/middleware/data.go | 2 + routers/common/middleware.go | 5 ++ routers/web/web.go | 4 -- services/webtheme/webtheme.go | 110 +++++++++++++++++------------ templates/base/footer_content.tmpl | 9 ++- vite.config.ts | 9 ++- web_src/css/home.css | 2 +- web_src/css/modules/navbar.css | 2 +- 10 files changed, 160 insertions(+), 75 deletions(-) diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 41e4ca7376..380c3ac455 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -9,7 +9,9 @@ import ( "io/fs" "os" "path/filepath" + "slices" "sort" + "strings" "time" "code.gitea.io/gitea/modules/container" @@ -61,6 +63,8 @@ type LayeredFS struct { layers []*Layer } +var _ fs.ReadDirFS = (*LayeredFS)(nil) + // Layered returns a new LayeredFS with the given layers. The first layer is the top layer. func Layered(layers ...*Layer) *LayeredFS { return &LayeredFS{layers: layers} @@ -83,6 +87,27 @@ func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) { return bs, err } +func (l *LayeredFS) ReadDir(name string) (files []fs.DirEntry, _ error) { + filesMap := map[string]fs.DirEntry{} + for _, layer := range l.layers { + entries, err := readDirOptional(layer, name) + if err != nil { + return nil, err + } + for _, entry := range entries { + entryName := entry.Name() + if _, exist := filesMap[entryName]; !exist && shouldInclude(entry) { + filesMap[entryName] = entry + } + } + } + for _, file := range filesMap { + files = append(files, file) + } + slices.SortFunc(files, func(a, b fs.DirEntry) int { return strings.Compare(a.Name(), b.Name()) }) + return files, nil +} + // ReadLayeredFile reads the named file, and returns the layer name. func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { name := util.PathJoinRel(elems...) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 9c8da951fc..25bd28a826 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -13,6 +13,7 @@ import ( "sync/atomic" "time" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/routing" @@ -22,24 +23,29 @@ const viteDevPortFile = "public/assets/.vite/dev-port" var viteDevProxy atomic.Pointer[httputil.ReverseProxy] +func getViteDevServerBaseURL() string { + portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) + portContent, _ := os.ReadFile(portFile) + port := strings.TrimSpace(string(portContent)) + if port == "" { + return "" + } + return "http://localhost:" + port +} + func getViteDevProxy() *httputil.ReverseProxy { if proxy := viteDevProxy.Load(); proxy != nil { return proxy } - portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) - data, err := os.ReadFile(portFile) - if err != nil { - return nil - } - port := strings.TrimSpace(string(data)) - if port == "" { + viteDevServerBaseURL := getViteDevServerBaseURL() + if viteDevServerBaseURL == "" { return nil } - target, err := url.Parse("http://localhost:" + port) + target, err := url.Parse(viteDevServerBaseURL) if err != nil { - log.Error("Failed to parse Vite dev server URL: %v", err) + log.Error("Failed to parse Vite dev server base URL %s, err: %v", viteDevServerBaseURL, err) return nil } @@ -60,7 +66,7 @@ func getViteDevProxy() *httputil.ReverseProxy { ModifyResponse: func(resp *http.Response) error { // add a header to indicate the Vite dev server port, // make developers know that this request is proxied to Vite dev server and which port it is - resp.Header.Add("X-Gitea-Vite-Port", port) + resp.Header.Add("X-Gitea-Vite-Dev-Server", viteDevServerBaseURL) return nil }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { @@ -92,19 +98,46 @@ func ViteDevMiddleware(next http.Handler) http.Handler { }) } -// isViteDevMode returns true if the Vite dev server port file exists. -// In production mode, the result is cached after the first check. -func isViteDevMode() bool { +var viteDevModeCheck atomic.Pointer[struct { + isDev bool + time time.Time +}] + +// IsViteDevMode returns true if the Vite dev server port file exists and the server is alive +func IsViteDevMode() bool { if setting.IsProd { return false } - portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) - _, err := os.Stat(portFile) - return err == nil + + now := time.Now() + lastCheck := viteDevModeCheck.Load() + if lastCheck != nil && time.Now().Sub(lastCheck.time) < time.Second { + return lastCheck.isDev + } + + viteDevServerBaseURL := getViteDevServerBaseURL() + if viteDevServerBaseURL == "" { + return false + } + + req := httplib.NewRequest(viteDevServerBaseURL+"/web_src/js/__vite_dev_server_check", "GET") + resp, _ := req.Response() + if resp != nil { + _ = resp.Body.Close() + } + isDev := resp != nil && resp.StatusCode == http.StatusOK + viteDevModeCheck.Store(&struct { + isDev bool + time time.Time + }{ + isDev: isDev, + time: now, + }) + return isDev } func viteDevSourceURL(name string) string { - if !isViteDevMode() { + if !IsViteDevMode() { return "" } if strings.HasPrefix(name, "css/theme-") { diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 41fb1e7e6f..7d9e816042 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -7,6 +7,7 @@ import ( "context" "time" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" ) @@ -36,5 +37,6 @@ func CommonTemplateContextData() reqctx.ContextData { "PageStartTime": time.Now(), "RunModeIsProd": setting.IsProd, + "ViteModeIsDev": public.IsViteDevMode(), } } diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 9daffb04f1..39911e2548 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/routing" @@ -40,6 +41,10 @@ func ProtocolMiddlewares() (handlers []any) { handlers = append(handlers, context.AccessLogger()) } + if !setting.IsProd { + handlers = append(handlers, public.ViteDevMiddleware) + } + return handlers } diff --git a/routers/web/web.go b/routers/web/web.go index 72d2c27eaf..e3dcf27cc4 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -259,10 +259,6 @@ func Routes() *web.Router { // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route routes.BeforeRouting(chi_middleware.GetHead) - if !setting.IsProd { - routes.BeforeRouting(public.ViteDevMiddleware) - } - routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, optionsCorsHandler(), public.FileHandlerFunc()) routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index 2f3d06d780..f8322381ca 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -4,10 +4,14 @@ package webtheme import ( + "io/fs" + "os" + "path" "regexp" "sort" "strings" - "sync" + "sync/atomic" + "time" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" @@ -16,15 +20,15 @@ import ( "code.gitea.io/gitea/modules/util" ) -type themeCollection struct { +type themeCollectionStruct struct { + lastCheckTime time.Time + usingViteDevMode bool + themeList []*ThemeMetaInfo themeMap map[string]*ThemeMetaInfo } -var ( - themeMu sync.RWMutex - availableThemes *themeCollection -) +var themeCollection atomic.Pointer[themeCollectionStruct] const ( fileNamePrefix = "theme-" @@ -140,23 +144,42 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo { return themeInfo } -func loadThemesFromAssets() (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) { - cssFiles, err := public.AssetFS().ListFiles("assets/css") +func collectThemeFiles(dirFS fs.ReadDirFS, fsPath string) (themes []*ThemeMetaInfo, _ error) { + files, err := dirFS.ReadDir(fsPath) if err != nil { - log.Error("Failed to list themes: %v", err) - return nil, nil + return nil, err + } + for _, file := range files { + fileName := file.Name() + if !strings.HasPrefix(fileName, fileNamePrefix) || !strings.HasSuffix(fileName, fileNameSuffix) { + continue + } + content, err := fs.ReadFile(dirFS, path.Join(fsPath, file.Name())) + if err != nil { + log.Error("Failed to read theme file %q: %v", fileName, err) + continue + } + themes = append(themes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content))) + } + return themes, nil +} + +func loadThemesFromAssets(isViteDevMode bool) (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) { + var themeDir fs.ReadDirFS + var themePath string + + if isViteDevMode { + // In vite dev mode, Vite serves themes directly from source files. + themeDir, themePath = os.DirFS(setting.StaticRootPath).(fs.ReadDirFS), "web_src/css/themes" + } else { + // Without vite dev server, use built assets from AssetFS. + themeDir, themePath = public.AssetFS(), "assets/css" } - var foundThemes []*ThemeMetaInfo - for _, fileName := range cssFiles { - if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) { - content, err := public.AssetFS().ReadFile("/assets/css/" + fileName) - if err != nil { - log.Error("Failed to read theme file %q: %v", fileName, err) - continue - } - foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content))) - } + foundThemes, err := collectThemeFiles(themeDir, themePath) + if err != nil { + log.Error("Failed to load theme files: %v", err) + return themeList, themeMap } themeList = foundThemes @@ -187,20 +210,21 @@ func loadThemesFromAssets() (themeList []*ThemeMetaInfo, themeMap map[string]*Th return themeList, themeMap } -func getAvailableThemes() (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) { - themeMu.RLock() - if availableThemes != nil { - themeList, themeMap = availableThemes.themeList, availableThemes.themeMap - } - themeMu.RUnlock() - if len(themeList) != 0 { - return themeList, themeMap +func getAvailableThemes() *themeCollectionStruct { + themes := themeCollection.Load() + + now := time.Now() + if themes != nil && now.Sub(themes.lastCheckTime) < time.Second { + return themes } - themeMu.Lock() - defer themeMu.Unlock() - // no need to double-check "availableThemes.themeList" since the loading isn't really slow, to keep code simple - themeList, themeMap = loadThemesFromAssets() + isViteDevMode := public.IsViteDevMode() + useLoadedThemes := themes != nil && (setting.IsProd || themes.usingViteDevMode == isViteDevMode) + if useLoadedThemes && len(themes.themeList) > 0 { + return themes + } + + themeList, themeMap := loadThemesFromAssets(isViteDevMode) hasAvailableThemes := len(themeList) > 0 if !hasAvailableThemes { defaultTheme := defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme) @@ -215,27 +239,19 @@ func getAvailableThemes() (themeList []*ThemeMetaInfo, themeMap map[string]*Them if themeMap[setting.UI.DefaultTheme] == nil { setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme) } - availableThemes = &themeCollection{themeList, themeMap} - return themeList, themeMap } - // In dev mode, only store the loaded themes if the list is not empty, in case the frontend is still being built. - // TBH, there still could be a data-race that the themes are only partially built then the list is incomplete for first time loading. - // Such edge case can be handled by checking whether the loaded themes are the same in a period or there is a flag file, but it is an over-kill, so, no. - if hasAvailableThemes { - availableThemes = &themeCollection{themeList, themeMap} - } - return themeList, themeMap -} - -func GetAvailableThemes() []*ThemeMetaInfo { - themes, _ := getAvailableThemes() + themes = &themeCollectionStruct{now, isViteDevMode, themeList, themeMap} + themeCollection.Store(themes) return themes } +func GetAvailableThemes() []*ThemeMetaInfo { + return getAvailableThemes().themeList +} + func GetThemeMetaInfo(internalName string) *ThemeMetaInfo { - _, themeMap := getAvailableThemes() - return themeMap[internalName] + return getAvailableThemes().themeMap[internalName] } // GuaranteeGetThemeMetaInfo guarantees to return a non-nil ThemeMetaInfo, diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl index 66c9d718ea..3b0af6ddc3 100644 --- a/templates/base/footer_content.tmpl +++ b/templates/base/footer_content.tmpl @@ -4,17 +4,22 @@ {{ctx.Locale.Tr "powered_by" "Gitea"}} {{end}} {{if (or .ShowFooterVersion .PageIsAdmin)}} + {{ctx.Locale.Tr "version"}}: {{if .IsAdmin}} {{AppVer}} {{else}} {{AppVer}} {{end}} + {{end}} {{if and .TemplateLoadTimes ShowFooterTemplateLoadTime}} - {{ctx.Locale.Tr "page"}}: {{LoadTimes .PageStartTime}} - {{ctx.Locale.Tr "template"}}{{if .TemplateName}} {{.TemplateName}}{{end}}: {{call .TemplateLoadTimes}} + + {{ctx.Locale.Tr "page"}}: {{LoadTimes .PageStartTime}} + {{ctx.Locale.Tr "template"}}{{if .TemplateName}} {{.TemplateName}}{{end}}: {{call .TemplateLoadTimes}} + {{end}} + {{if $.ViteModeIsDev}}ViteDevMode{{end}}