Merge pull request 'fix: lowercase Joomla element names, rename Manifest → Metadata (#635)' (#644) from fix/635-joomla-element-name into main
Deploy MokoGitea / deploy (push) Failing after 3m22s

This commit was merged in pull request #644.
This commit is contained in:
2026-06-18 21:05:39 +00:00
7 changed files with 75 additions and 62 deletions
+37 -24
View File
@@ -12,13 +12,13 @@ import (
)
func init() {
db.RegisterModel(new(RepoManifest))
db.RegisterModel(new(RepoMetadata))
}
// RepoManifest stores mokoplatform manifest settings for a repository.
// RepoMetadata stores mokoplatform metadata settings for a repository.
// These fields correspond to the .mokogitea/manifest.xml schema and are
// exposed via API for use by Actions workflows and the mokoplatform CLI.
type RepoManifest struct {
type RepoMetadata struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
@@ -55,13 +55,13 @@ type RepoManifest struct {
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (RepoManifest) TableName() string {
func (RepoMetadata) TableName() string {
return "repo_manifest"
}
// joomlaTypePrefix maps Joomla extension types to their element name prefixes.
// Plugins have no prefix in Joomla's #__extensions table — the element is the
// lowercased, hyphen-free name, and the folder column determines the plugin group.
// Derived from Joomla 6 source: libraries/src/Installer/Adapter/*Adapter.php
// Plugins use bare names (no prefix) — the folder column determines the plugin group.
var joomlaTypePrefix = map[string]string{
"component": "com_",
"module": "mod_",
@@ -71,22 +71,35 @@ var joomlaTypePrefix = map[string]string{
"file": "file_",
}
// AutoElementName returns the auto-constructed Joomla element name (e.g. pkg_mokowaas).
// The name is lowercased and hyphens are removed to produce clean element names
// for the #__extensions.element column (e.g. "MokoSuiteBackup" → "pkg_mokosuitebackup").
func (m *RepoManifest) AutoElementName() string {
// cleanJoomlaElement replicates Joomla's InputFilter::clean($value, 'cmd')
// which lowercases and strips all characters except [a-z0-9._-].
func cleanJoomlaElement(name string) string {
lower := strings.ToLower(name)
var b strings.Builder
b.Grow(len(lower))
for _, r := range lower {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
b.WriteRune(r)
}
}
return b.String()
}
// AutoElementName returns the auto-constructed Joomla element name (e.g. pkg_mokosuitebackup).
// The name is lowercased and cleaned to match Joomla's InputFilter::clean('cmd') behavior.
func (m *RepoMetadata) AutoElementName() string {
if m.Name == "" || m.PackageType == "" {
return ""
}
lower := strings.ToLower(strings.ReplaceAll(m.Name, "-", ""))
cleaned := cleanJoomlaElement(m.Name)
if prefix, ok := joomlaTypePrefix[m.PackageType]; ok {
return prefix + lower
return prefix + cleaned
}
return lower
return cleaned
}
// FullElementName returns the effective element name: override if set, otherwise auto-constructed.
func (m *RepoManifest) FullElementName() string {
func (m *RepoMetadata) FullElementName() string {
if m.ElementName != "" {
return m.ElementName
}
@@ -94,7 +107,7 @@ func (m *RepoManifest) FullElementName() string {
}
// ElementNameMismatch returns true if an override is set that differs from the auto-constructed name.
func (m *RepoManifest) ElementNameMismatch() bool {
func (m *RepoMetadata) ElementNameMismatch() bool {
if m.ElementName == "" {
return false
}
@@ -102,9 +115,9 @@ func (m *RepoManifest) ElementNameMismatch() bool {
return auto != "" && m.ElementName != auto
}
// GetRepoManifest returns the manifest for a repo, or nil if none exists.
func GetRepoManifest(ctx context.Context, repoID int64) (*RepoManifest, error) {
m := new(RepoManifest)
// GetRepoMetadata returns the metadata for a repo, or nil if none exists.
func GetRepoMetadata(ctx context.Context, repoID int64) (*RepoMetadata, error) {
m := new(RepoMetadata)
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(m)
if err != nil {
return nil, err
@@ -115,9 +128,9 @@ func GetRepoManifest(ctx context.Context, repoID int64) (*RepoManifest, error) {
return m, nil
}
// CreateOrUpdateRepoManifest upserts a repo manifest.
func CreateOrUpdateRepoManifest(ctx context.Context, m *RepoManifest) error {
existing := new(RepoManifest)
// CreateOrUpdateRepoMetadata upserts a repo metadata.
func CreateOrUpdateRepoMetadata(ctx context.Context, m *RepoMetadata) error {
existing := new(RepoMetadata)
has, err := db.GetEngine(ctx).Where("repo_id = ?", m.RepoID).Get(existing)
if err != nil {
return err
@@ -131,8 +144,8 @@ func CreateOrUpdateRepoManifest(ctx context.Context, m *RepoManifest) error {
return err
}
// DeleteRepoManifest deletes the manifest for a repo.
func DeleteRepoManifest(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(RepoManifest))
// DeleteRepoMetadata deletes the metadata for a repo.
func DeleteRepoMetadata(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(RepoMetadata))
return err
}
+4 -4
View File
@@ -1480,11 +1480,11 @@ func Routes() *web.Router {
}, reqAdmin())
}, reqAnyRepoReader())
m.Combo("/metadata", reqRepoReader(unit.TypeCode)).
Get(repo.GetRepoManifest).
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
Get(repo.GetRepoMetadata).
Put(reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
m.Combo("/manifest", reqRepoReader(unit.TypeCode)). // backward compat
Get(repo.GetRepoManifest).
Put(reqToken(), reqAdmin(), repo.UpdateRepoManifest)
Get(repo.GetRepoMetadata).
Put(reqToken(), reqAdmin(), repo.UpdateRepoMetadata)
// MokoGitea badge engine
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
+13 -13
View File
@@ -11,8 +11,8 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// apiManifest is the JSON representation of a repo manifest.
type apiManifest struct {
// apiMetadata is the JSON representation of a repo manifest.
type apiMetadata struct {
Name string `json:"name"`
Org string `json:"org"`
Description string `json:"description"`
@@ -34,8 +34,8 @@ type apiManifest struct {
EntryPoint string `json:"entry_point"`
}
// GetRepoManifest returns the manifest settings for a repository.
func GetRepoManifest(ctx *context.APIContext) {
// GetRepoMetadata returns the manifest settings for a repository.
func GetRepoMetadata(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/manifest repository repoGetManifest
// ---
// summary: Get repo manifest settings
@@ -46,21 +46,21 @@ func GetRepoManifest(ctx *context.APIContext) {
// "$ref": "#/responses/Manifest"
// "404":
// "$ref": "#/responses/notFound"
m, err := repo_model.GetRepoManifest(ctx, ctx.Repo.Repository.ID)
m, err := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if m == nil {
// Return defaults from repo metadata.
ctx.JSON(http.StatusOK, &apiManifest{
ctx.JSON(http.StatusOK, &apiMetadata{
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
Description: ctx.Repo.Repository.Description,
})
return
}
ctx.JSON(http.StatusOK, &apiManifest{
ctx.JSON(http.StatusOK, &apiMetadata{
Name: m.Name,
Org: m.Org,
Description: m.Description,
@@ -84,8 +84,8 @@ func GetRepoManifest(ctx *context.APIContext) {
})
}
// UpdateRepoManifest updates the manifest settings for a repository.
func UpdateRepoManifest(ctx *context.APIContext) {
// UpdateRepoMetadata updates the manifest settings for a repository.
func UpdateRepoMetadata(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/manifest repository repoUpdateManifest
// ---
// summary: Update repo manifest settings
@@ -96,13 +96,13 @@ func UpdateRepoManifest(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/Manifest"
var req apiManifest
var req apiMetadata
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
m := &repo_model.RepoManifest{
m := &repo_model.RepoMetadata{
RepoID: ctx.Repo.Repository.ID,
Name: req.Name,
Org: req.Org,
@@ -126,12 +126,12 @@ func UpdateRepoManifest(ctx *context.APIContext) {
EntryPoint: req.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, m); err != nil {
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, m); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, &apiManifest{
ctx.JSON(http.StatusOK, &apiMetadata{
Name: m.Name,
Org: m.Org,
Description: m.Description,
+10 -10
View File
@@ -26,13 +26,13 @@ func Metadata(ctx *context.Context) {
ownerID := ctx.Repo.Repository.OwnerID
// Load manifest (project identity).
manifest, err := repo_model.GetRepoManifest(ctx, repoID)
manifest, err := repo_model.GetRepoMetadata(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepoManifest", err)
ctx.ServerError("GetRepoMetadata", err)
return
}
if manifest == nil {
manifest = &repo_model.RepoManifest{
manifest = &repo_model.RepoMetadata{
RepoID: repoID,
Name: ctx.Repo.Repository.Name,
Org: ctx.Repo.Repository.OwnerName,
@@ -68,11 +68,11 @@ func Metadata(ctx *context.Context) {
func MetadataPost(ctx *context.Context) {
switch ctx.FormString("action") {
case "manifest":
saveManifest(ctx)
saveMetadata(ctx)
case "customfields":
saveCustomFields(ctx)
default:
saveManifest(ctx)
saveMetadata(ctx)
}
}
@@ -92,14 +92,14 @@ var spdxToName = map[string]string{
"proprietary": "Proprietary",
}
func saveManifest(ctx *context.Context) {
func saveMetadata(ctx *context.Context) {
spdx := ctx.FormString("license_spdx")
licenseName := spdxToName[spdx]
// Preserve existing values for fields removed from the UI.
existing, _ := repo_model.GetRepoManifest(ctx, ctx.Repo.Repository.ID)
existing, _ := repo_model.GetRepoMetadata(ctx, ctx.Repo.Repository.ID)
manifest := &repo_model.RepoManifest{
manifest := &repo_model.RepoMetadata{
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
@@ -127,8 +127,8 @@ func saveManifest(ctx *context.Context) {
manifest.Language = existing.Language
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoManifest", err)
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
ctx.ServerError("CreateOrUpdateRepoMetadata", err)
return
}
+8 -8
View File
@@ -57,11 +57,11 @@ type manifestBuild struct {
EntryPoint string `xml:"entry-point"`
}
// SyncManifestFromCommit reads .mokogitea/manifest.xml from the given commit
// SyncMetadataFromCommit reads .mokogitea/manifest.xml from the given commit
// and upserts the values into the repo_manifest database table.
// This is called on push to the default branch to keep the database in sync
// with the XML file. If no manifest.xml exists, this is a no-op.
func SyncManifestFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
func SyncMetadataFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
if commit == nil {
return
}
@@ -73,7 +73,7 @@ func SyncManifestFromCommit(ctx context.Context, repo *repo_model.Repository, co
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Error("SyncManifest: read blob for %s: %v", repo.FullName(), err)
log.Error("SyncMetadata: read blob for %s: %v", repo.FullName(), err)
return
}
defer reader.Close()
@@ -81,11 +81,11 @@ func SyncManifestFromCommit(ctx context.Context, repo *repo_model.Repository, co
var mxml manifestXML
decoder := xml.NewDecoder(reader)
if err := decoder.Decode(&mxml); err != nil {
log.Error("SyncManifest: parse XML for %s: %v", repo.FullName(), err)
log.Error("SyncMetadata: parse XML for %s: %v", repo.FullName(), err)
return
}
manifest := &repo_model.RepoManifest{
manifest := &repo_model.RepoMetadata{
RepoID: repo.ID,
Name: mxml.Identity.Name,
Org: mxml.Identity.Org,
@@ -109,10 +109,10 @@ func SyncManifestFromCommit(ctx context.Context, repo *repo_model.Repository, co
EntryPoint: mxml.Build.EntryPoint,
}
if err := repo_model.CreateOrUpdateRepoManifest(ctx, manifest); err != nil {
log.Error("SyncManifest: save for %s: %v", repo.FullName(), err)
if err := repo_model.CreateOrUpdateRepoMetadata(ctx, manifest); err != nil {
log.Error("SyncMetadata: save for %s: %v", repo.FullName(), err)
return
}
log.Info("SyncManifest: synced .mokogitea/manifest.xml for %s", repo.FullName())
log.Info("SyncMetadata: synced .mokogitea/manifest.xml for %s", repo.FullName())
}
+1 -1
View File
@@ -195,7 +195,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
log.Error("DelRepoDivergenceFromCache: %v", err)
}
// Auto-sync .mokogitea/manifest.xml to database on default branch push
SyncManifestFromCommit(ctx, repo, newCommit)
SyncMetadataFromCommit(ctx, repo, newCommit)
// Run security scanners on default branch push
security_service.ScanOnPush(ctx, repo, newCommit)
} else {
+2 -2
View File
@@ -187,9 +187,9 @@ func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository,
}
// Manifest is the source of truth for extension metadata.
manifest, err := repo_model.GetRepoManifest(ctx, repo.ID)
manifest, err := repo_model.GetRepoMetadata(ctx, repo.ID)
if err != nil {
log.Error("resolveExtensionMetadata: GetRepoManifest for repo %d: %v", repo.ID, err)
log.Error("resolveExtensionMetadata: GetRepoMetadata for repo %d: %v", repo.ID, err)
}
if manifest != nil {
if elem := manifest.FullElementName(); elem != "" {