Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 506e4a49d6 |
@@ -3,6 +3,7 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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: 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 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)
|
- 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)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,6 +168,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
|||||||
}
|
}
|
||||||
loadUIFrom(cfg)
|
loadUIFrom(cfg)
|
||||||
loadAdminFrom(cfg)
|
loadAdminFrom(cfg)
|
||||||
|
loadMetadataFrom(cfg)
|
||||||
loadAPIFrom(cfg)
|
loadAPIFrom(cfg)
|
||||||
loadMetricsFrom(cfg)
|
loadMetricsFrom(cfg)
|
||||||
loadCamoFrom(cfg)
|
loadCamoFrom(cfg)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
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/modules/templates"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
)
|
)
|
||||||
@@ -43,6 +44,25 @@ func Metadata(ctx *context.Context) {
|
|||||||
// It is displayed read-only. See issue #771.
|
// It is displayed read-only. See issue #771.
|
||||||
ctx.Data["DerivedOrg"] = ctx.Repo.Repository.DerivedOrgName(ctx)
|
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.
|
// Load repo-scoped custom fields.
|
||||||
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
|
fields, _ := issues_model.GetCustomFieldsByOwner(ctx, ownerID, issues_model.CustomFieldScopeRepo)
|
||||||
ctx.Data["CustomFieldDefs"] = fields
|
ctx.Data["CustomFieldDefs"] = fields
|
||||||
@@ -102,8 +122,8 @@ func saveMetadata(ctx *context.Context) {
|
|||||||
existing, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
|
existing, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
|
||||||
|
|
||||||
manifest := &repo_model.RepoMetadata{
|
manifest := &repo_model.RepoMetadata{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
Name: ctx.FormString("name"),
|
Name: ctx.FormString("name"),
|
||||||
// org is not stored: it is derived from the org profile on read. See issue #771.
|
// org is not stored: it is derived from the org profile on read. See issue #771.
|
||||||
Description: ctx.Repo.Repository.Description,
|
Description: ctx.Repo.Repository.Description,
|
||||||
LicenseSPDX: spdx,
|
LicenseSPDX: spdx,
|
||||||
@@ -113,7 +133,7 @@ func saveMetadata(ctx *context.Context) {
|
|||||||
InfoURL: ctx.FormString("info_url"),
|
InfoURL: ctx.FormString("info_url"),
|
||||||
TargetVersion: ctx.FormString("target_version"),
|
TargetVersion: ctx.FormString("target_version"),
|
||||||
PHPMinimum: ctx.FormString("php_minimum"),
|
PHPMinimum: ctx.FormString("php_minimum"),
|
||||||
ExtensionType: ctx.FormString("extension_type"),
|
ExtensionType: ctx.FormString("extension_type"),
|
||||||
EntryPoint: ctx.FormString("entry_point"),
|
EntryPoint: ctx.FormString("entry_point"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,4 +180,3 @@ func saveCustomFields(ctx *context.Context) {
|
|||||||
ctx.Flash.Success("Custom fields saved")
|
ctx.Flash.Success("Custom fields saved")
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -786,6 +786,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Get("/reset", admin.BrandingReset)
|
m.Get("/reset", admin.BrandingReset)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.Group("/metadata", func() {
|
||||||
|
m.Get("", admin.Metadata)
|
||||||
|
m.Post("/settings", admin.MetadataSettings)
|
||||||
|
})
|
||||||
|
|
||||||
m.Group("/monitor", func() {
|
m.Group("/monitor", func() {
|
||||||
m.Get("/stats", admin.MonitorStats)
|
m.Get("/stats", admin.MonitorStats)
|
||||||
m.Get("/cron", admin.CronTasks)
|
m.Get("/cron", admin.CronTasks)
|
||||||
@@ -1239,7 +1244,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
}, repo_setting.SettingsCtxData)
|
}, repo_setting.SettingsCtxData)
|
||||||
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
|
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
|
||||||
m.Combo("/updateserver").Get(repo_setting.UpdateServerSettings).Post(repo_setting.UpdateServerSettingsPost)
|
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.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost) // redirect
|
||||||
m.Group("/security", func() {
|
m.Group("/security", func() {
|
||||||
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
|
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{{template "admin/layout_head" (dict "pageClass" "admin metadata")}}
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{svg "octicon-list-unordered" 16}} Metadata
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<h5>Platform Options</h5>
|
||||||
|
<p class="tw-text-text-light tw-text-sm">Allowed values for the repository metadata <strong>Platform</strong> field. One per line (or comma-separated). Changes take effect immediately and are saved to <code>app.ini</code> <code>[metadata] PLATFORM_OPTIONS</code>.</p>
|
||||||
|
<form method="post" action="{{AppSubUrl}}/-/admin/metadata/settings">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="field">
|
||||||
|
<textarea name="platform_options" rows="6" class="tw-w-full tw-font-mono" placeholder="joomla dolibarr go npm generic">{{.PlatformOptions}}</textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="ui primary small button tw-mt-2">{{svg "octicon-check" 14}} Save Settings</button>
|
||||||
|
</form>
|
||||||
|
<p class="tw-text-text-light tw-text-sm tw-mt-3">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.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -87,6 +87,9 @@
|
|||||||
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
|
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
|
||||||
{{svg "octicon-paintbrush" 16}} Branding
|
{{svg "octicon-paintbrush" 16}} Branding
|
||||||
</a>
|
</a>
|
||||||
|
<a class="{{if .PageIsAdminMetadata}}active {{end}}item" href="{{AppSubUrl}}/-/admin/metadata">
|
||||||
|
{{svg "octicon-list-unordered" 16}} Metadata
|
||||||
|
</a>
|
||||||
<a class="{{if .PageIsAdminLicenseTiers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/license-tiers">
|
<a class="{{if .PageIsAdminLicenseTiers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/license-tiers">
|
||||||
{{svg "octicon-key" 16}} License Tiers
|
{{svg "octicon-key" 16}} License Tiers
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<select name="platform" class="ui dropdown">
|
<select name="platform" class="ui dropdown">
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
{{$platform := .Manifest.Platform}}
|
{{$platform := .Manifest.Platform}}
|
||||||
{{range $val := StringUtils.Split "joomla,wordpress,dolibarr,go,mcp,platform,generic" ","}}
|
{{range $val := .PlatformOptions}}
|
||||||
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
|
<option value="{{$val}}" {{if eq $val $platform}}selected{{end}}>{{$val}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
Reference in New Issue
Block a user