From 506e4a49d658c47bb52a796d1f490cc97badd900 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 13 Jul 2026 17:27:03 -0500 Subject: [PATCH] feat(metadata): admin-configurable platform dropdown options The repo metadata 'platform' field was a hardcoded enum; changing the taxonomy meant a code change + redeploy. Make it admin-configurable: - modules/setting/metadata.go: new [metadata] PLATFORM_OPTIONS setting (default joomla,dolibarr,go,npm,generic), loaded in loadCommonSettingsFrom - routers/web/admin/metadata.go + templates/admin/metadata.tmpl: new Admin -> Metadata page to edit the list (persisted to app.ini), with a nav entry and route - routers/web/repo/setting/metadata.go + template: platform dropdown now reads from the setting; a repo's current value stays selectable even if removed from the list (no silent drop) Closes #777 --- CHANGELOG.md | 1 + modules/setting/metadata.go | 22 +++++++++ modules/setting/setting.go | 1 + routers/web/admin/metadata.go | 71 +++++++++++++++++++++++++++ routers/web/repo/setting/metadata.go | 27 ++++++++-- routers/web/web.go | 7 ++- templates/admin/metadata.tmpl | 19 +++++++ templates/admin/navbar.tmpl | 3 ++ templates/repo/settings/metadata.tmpl | 2 +- 9 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 modules/setting/metadata.go create mode 100644 routers/web/admin/metadata.go create mode 100644 templates/admin/metadata.tmpl diff --git a/CHANGELOG.md b/CHANGELOG.md index ee80b4d91b..732af72e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Added +- Metadata platform options are now **admin-configurable** instead of hardcoded: a new **Admin → Metadata** page edits the allowed `platform` values (persisted to `app.ini` `[metadata] PLATFORM_OPTIONS`, default `joomla,dolibarr,go,npm,generic`), and the repo Settings → Metadata platform dropdown reads from it. Changing the taxonomy no longer needs a code change/redeploy. A repo's existing platform value stays selectable even if later removed from the list (#777) - Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727) - Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727) - Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727) diff --git a/modules/setting/metadata.go b/modules/setting/metadata.go new file mode 100644 index 0000000000..777f681e35 --- /dev/null +++ b/modules/setting/metadata.go @@ -0,0 +1,22 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package setting + +// Metadata holds instance-level settings for the repository metadata feature. +var Metadata = struct { + // PlatformOptions is the admin-configurable list of allowed values for the + // repo metadata "platform" field. Editable via Admin -> Metadata (persisted + // to app.ini [metadata] PLATFORM_OPTIONS) so changing the taxonomy needs no + // code change or redeploy. See issue #777. + PlatformOptions []string +}{ + PlatformOptions: []string{"joomla", "dolibarr", "go", "npm", "generic"}, +} + +func loadMetadataFrom(rootCfg ConfigProvider) { + sec := rootCfg.Section("metadata") + if opts := sec.Key("PLATFORM_OPTIONS").Strings(","); len(opts) > 0 { + Metadata.PlatformOptions = opts + } +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 6ed0e09a25..242904bfde 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -168,6 +168,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error { } loadUIFrom(cfg) loadAdminFrom(cfg) + loadMetadataFrom(cfg) loadAPIFrom(cfg) loadMetricsFrom(cfg) loadCamoFrom(cfg) diff --git a/routers/web/admin/metadata.go b/routers/web/admin/metadata.go new file mode 100644 index 0000000000..814ed034d1 --- /dev/null +++ b/routers/web/admin/metadata.go @@ -0,0 +1,71 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package admin + +import ( + "net/http" + "strings" + + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" +) + +const tplMetadata templates.TplName = "admin/metadata" + +// Metadata shows the admin metadata-settings page. +func Metadata(ctx *context.Context) { + ctx.Data["Title"] = "Metadata" + ctx.Data["PageIsAdminMetadata"] = true + ctx.Data["PlatformOptions"] = strings.Join(setting.Metadata.PlatformOptions, "\n") + ctx.HTML(http.StatusOK, tplMetadata) +} + +// parsePlatformOptions normalizes admin input (comma- or newline-separated) into +// a clean, lowercased, de-duplicated, order-preserving list of platform values. +func parsePlatformOptions(raw string) []string { + seen := make(map[string]bool) + var out []string + for _, tok := range strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' }) { + v := strings.ToLower(strings.TrimSpace(tok)) + if v == "" || seen[v] { + continue + } + seen[v] = true + out = append(out, v) + } + return out +} + +// MetadataSettings persists the admin metadata settings. +func MetadataSettings(ctx *context.Context) { + opts := parsePlatformOptions(ctx.FormString("platform_options")) + if len(opts) == 0 { + ctx.Flash.Error("Platform options cannot be empty") + ctx.Redirect(setting.AppSubURL + "/-/admin/metadata") + return + } + + // Apply in-memory so the change takes effect without a restart. + setting.Metadata.PlatformOptions = opts + + // Persist to app.ini [metadata] PLATFORM_OPTIONS. + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) + if err != nil { + ctx.Flash.Error("Failed to load config: " + err.Error()) + ctx.Redirect(setting.AppSubURL + "/-/admin/metadata") + return + } + cfg.Section("metadata").Key("PLATFORM_OPTIONS").SetValue(strings.Join(opts, ",")) + if err := cfg.SaveTo(setting.CustomConf); err != nil { + ctx.Flash.Error("Failed to save config: " + err.Error()) + log.Error("SaveTo %s: %v", setting.CustomConf, err) + } else { + ctx.Flash.Success("Metadata settings saved") + log.Info("Metadata platform options updated: %v", opts) + } + + ctx.Redirect(setting.AppSubURL + "/-/admin/metadata") +} diff --git a/routers/web/repo/setting/metadata.go b/routers/web/repo/setting/metadata.go index 6bbea3617a..58abe8c548 100644 --- a/routers/web/repo/setting/metadata.go +++ b/routers/web/repo/setting/metadata.go @@ -10,6 +10,7 @@ import ( issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" + "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" ) @@ -43,6 +44,25 @@ func Metadata(ctx *context.Context) { // It is displayed read-only. See issue #771. ctx.Data["DerivedOrg"] = ctx.Repo.Repository.DerivedOrgName(ctx) + // Platform dropdown options come from the admin-configurable setting + // (Admin -> Metadata). Keep the repo's current value selectable even if it + // was later removed from the list, so saving does not silently drop it. + // See issue #777. + platformOpts := append([]string(nil), setting.Metadata.PlatformOptions...) + if manifest.Platform != "" { + found := false + for _, p := range platformOpts { + if p == manifest.Platform { + found = true + break + } + } + if !found { + platformOpts = append([]string{manifest.Platform}, platformOpts...) + } + } + ctx.Data["PlatformOptions"] = platformOpts + // Load repo-scoped custom fields. fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo) ctx.Data["CustomFieldDefs"] = fields @@ -102,8 +122,8 @@ func saveMetadata(ctx *context.Context) { existing, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID) manifest := &repo_model.RepoMetadata{ - RepoID: ctx.Repo.Repository.ID, - Name: ctx.FormString("name"), + RepoID: ctx.Repo.Repository.ID, + Name: ctx.FormString("name"), // org is not stored: it is derived from the org profile on read. See issue #771. Description: ctx.Repo.Repository.Description, LicenseSPDX: spdx, @@ -113,7 +133,7 @@ func saveMetadata(ctx *context.Context) { InfoURL: ctx.FormString("info_url"), TargetVersion: ctx.FormString("target_version"), PHPMinimum: ctx.FormString("php_minimum"), - ExtensionType: ctx.FormString("extension_type"), + ExtensionType: ctx.FormString("extension_type"), EntryPoint: ctx.FormString("entry_point"), } @@ -160,4 +180,3 @@ func saveCustomFields(ctx *context.Context) { ctx.Flash.Success("Custom fields saved") ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata") } - diff --git a/routers/web/web.go b/routers/web/web.go index 8f9840a22f..0b3f86f90a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -786,6 +786,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/reset", admin.BrandingReset) }) + m.Group("/metadata", func() { + m.Get("", admin.Metadata) + m.Post("/settings", admin.MetadataSettings) + }) + m.Group("/monitor", func() { m.Get("/stats", admin.MonitorStats) m.Get("/cron", admin.CronTasks) @@ -1239,7 +1244,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { }, repo_setting.SettingsCtxData) m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost) m.Combo("/updateserver").Get(repo_setting.UpdateServerSettings).Post(repo_setting.UpdateServerSettingsPost) - m.Combo("/manifest").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect + m.Combo("/manifest").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect m.Group("/security", func() { m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost) diff --git a/templates/admin/metadata.tmpl b/templates/admin/metadata.tmpl new file mode 100644 index 0000000000..e9061e36d9 --- /dev/null +++ b/templates/admin/metadata.tmpl @@ -0,0 +1,19 @@ +{{template "admin/layout_head" (dict "pageClass" "admin metadata")}} +
+

+ {{svg "octicon-list-unordered" 16}} Metadata +

+
+
Platform Options
+

Allowed values for the repository metadata Platform field. One per line (or comma-separated). Changes take effect immediately and are saved to app.ini [metadata] PLATFORM_OPTIONS.

+
+ {{.CsrfTokenHtml}} +
+ +
+ +
+

Existing repositories keep their stored platform value even if it is later removed from this list; it simply won't be offered to other repos.

+
+
+{{template "admin/layout_footer" .}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 943a279fa7..30df5df0d0 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -87,6 +87,9 @@ {{svg "octicon-paintbrush" 16}} Branding + + {{svg "octicon-list-unordered" 16}} Metadata + {{svg "octicon-key" 16}} License Tiers diff --git a/templates/repo/settings/metadata.tmpl b/templates/repo/settings/metadata.tmpl index 5d27acaf09..8914ac430b 100644 --- a/templates/repo/settings/metadata.tmpl +++ b/templates/repo/settings/metadata.tmpl @@ -55,7 +55,7 @@ -- 2.52.0