From aeb36e43124b4e67c98619b96d27eadb1abfacf5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 9 Jun 2026 14:32:39 -0500 Subject: [PATCH 1/2] feat: use manifest API as source of truth for update feed metadata (#592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace custom field + config table cascade with manifest-only read for extension identity fields (element_name, package_type, display_name, target_version, php_minimum, description). Config table retained only for licensing fields (download_gating, key_prefix, support_url fallback). Fix client field: package/component/library/file → administrator. Remove issues_model import (custom field lookups removed). --- services/updateserver/joomla.go | 128 +++++++++++--------------------- 1 file changed, 43 insertions(+), 85 deletions(-) diff --git a/services/updateserver/joomla.go b/services/updateserver/joomla.go index b64edf9fd1..10d5b23e4f 100644 --- a/services/updateserver/joomla.go +++ b/services/updateserver/joomla.go @@ -13,7 +13,6 @@ import ( "time" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db" - issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses" repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" @@ -163,7 +162,7 @@ func NormalizeChannel(ch string) string { } // extensionMetadata holds resolved metadata for feed generation. -// Fields are resolved with priority: custom field → config table → default. +// Fields are resolved with priority: manifest → config table (gating only) → default. type extensionMetadata struct { Element string DisplayName string @@ -176,8 +175,9 @@ type extensionMetadata struct { KeyPrefix string } -// resolveExtensionMetadata loads extension metadata with cascading fallback: -// org-level repo-scoped custom fields → update_stream_config → repo-derived defaults. +// resolveExtensionMetadata loads extension metadata from the repo manifest API. +// The manifest is the single source of truth for extension identity fields. +// The config table is only used for licensing/gating fields not in the manifest. func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, cfg *licenses.UpdateStreamConfig) extensionMetadata { m := extensionMetadata{ Element: strings.ToLower(repo.Name), @@ -186,91 +186,49 @@ func resolveExtensionMetadata(ctx context.Context, repo *repo_model.Repository, TargetVersion: "(5|6)\\..*", } - // Apply config table values. + // Manifest is the source of truth for extension metadata. + manifest, err := repo_model.GetRepoManifest(ctx, repo.ID) + if err != nil { + log.Error("resolveExtensionMetadata: GetRepoManifest for repo %d: %v", repo.ID, err) + } + if manifest != nil { + if manifest.ElementName != "" { + m.Element = manifest.ElementName + } + if manifest.PackageType != "" { + m.ExtType = manifest.PackageType + } + if manifest.DisplayName != "" { + m.DisplayName = manifest.DisplayName + } + if manifest.TargetVersion != "" { + m.TargetVersion = manifest.TargetVersion + } + if manifest.PHPMinimum != "" { + m.PHPMinimum = manifest.PHPMinimum + } + if manifest.Description != "" { + m.Description = manifest.Description + } + if manifest.InfoURL != "" { + m.SupportURL = manifest.InfoURL + } + } + + // Config table: only licensing/gating fields (not in manifest). if cfg != nil { - if cfg.ExtensionName != "" { - m.Element = cfg.ExtensionName - } - if cfg.DisplayName != "" { - m.DisplayName = cfg.DisplayName - } - if cfg.ExtensionType != "" { - m.ExtType = cfg.ExtensionType - } - if cfg.TargetVersion != "" { - m.TargetVersion = cfg.TargetVersion - } - if cfg.PHPMinimum != "" { - m.PHPMinimum = cfg.PHPMinimum - } - if cfg.Description != "" { - m.Description = cfg.Description - } - if cfg.SupportURL != "" { - m.SupportURL = cfg.SupportURL - } if cfg.DownloadGating != "" { m.DownloadGating = cfg.DownloadGating } if cfg.KeyPrefix != "" { m.KeyPrefix = cfg.KeyPrefix } - } - - // Override with custom field values (highest priority). - fields, err := issues_model.GetCustomFieldsByOwner(ctx, repo.OwnerID, issues_model.CustomFieldScopeRepo) - if err != nil { - log.Error("resolveExtensionMetadata: GetCustomFieldsByOwner for repo %d: %v", repo.ID, err) - return m - } - if len(fields) == 0 { - return m - } - values, err := issues_model.GetCustomFieldValuesMap(ctx, repo.ID) - if err != nil { - log.Error("resolveExtensionMetadata: GetCustomFieldValuesMap for repo %d: %v", repo.ID, err) - return m - } - if len(values) == 0 { - return m - } - - // Build name → value map from field definitions + values. - named := make(map[string]string, len(fields)) - for _, f := range fields { - if v, ok := values[f.ID]; ok && v != "" { - named[f.Name] = v + // SupportURL from config as fallback if manifest.InfoURL is empty + if m.SupportURL == "" && cfg.SupportURL != "" { + m.SupportURL = cfg.SupportURL } } - if v := named["Extension Name"]; v != "" { - m.Element = v - } - if v := named["Display Name"]; v != "" { - m.DisplayName = v - } - if v := named["Extension Type"]; v != "" { - m.ExtType = v - } - if v := named["Target Version"]; v != "" { - m.TargetVersion = v - } - if v := named["PHP Minimum"]; v != "" { - m.PHPMinimum = v - } - if v := named["Support URL"]; v != "" { - m.SupportURL = v - } - if v := named["Description"]; v != "" { - m.Description = v - } - if v := named["Download Gating"]; v != "" { - m.DownloadGating = v - } - if v := named["Key Prefix"]; v != "" { - m.KeyPrefix = v - } - return m } @@ -422,13 +380,13 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require infoURL = meta.SupportURL } - // Joomla element: packages use client_id=0 in #__extensions, - // so we must output 0 for Joomla to match the update - // to the installed extension. Other types default to "site" (client_id=0) - // or "administrator" (client_id=1). + // Joomla element: admin-side extensions use "administrator", + // site-side extensions use "site". Packages, components, libraries, + // and files are admin-side by default. client := "site" - if extType == "package" { - client = "0" + switch extType { + case "package", "component", "library", "file": + client = "administrator" } u := xmlUpdate{ -- 2.52.0 From a506e3995514497903cf55e9a1b475eead555fc4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 9 Jun 2026 15:02:44 -0500 Subject: [PATCH 2/2] fix: switch org wiki tab from standalone repos to .profile wiki sidecars The org wiki tab previously read from standalone `wiki` and `wiki-private` repos. Those repos have been migrated into the `.profile` and `.profile-private` repo wikis (Gitea wiki sidecars). - Update RepoNameWikiPublic/Private constants to .profile/.profile-private - findOrgWikiCommit now opens WikiStorageRepo() instead of main repo - OrgWikiRepoExists checks wiki sidecar via GetDefaultBranch - Update empty state and settings template text Fixes #594 --- routers/web/org/wiki.go | 20 +++++++++++--------- routers/web/shared/user/header.go | 12 +++++++----- templates/org/settings/options.tmpl | 4 ++-- templates/org/wiki/view.tmpl | 4 ++-- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/routers/web/org/wiki.go b/routers/web/org/wiki.go index c7fe841d0a..8a7280b31c 100644 --- a/routers/web/org/wiki.go +++ b/routers/web/org/wiki.go @@ -157,7 +157,8 @@ func Wiki(ctx *context.Context) { ctx.HTML(http.StatusOK, tplOrgWiki) } -// findOrgWikiCommit locates the convention wiki repo and returns its HEAD commit. +// findOrgWikiCommit locates the profile repo's wiki and returns its HEAD commit. +// The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .profile.wiki.git). func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) { dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName) if err != nil { @@ -167,19 +168,20 @@ func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*rep return nil, nil } - if dbRepo.IsEmpty { + // Open the wiki git repo (.wiki.git sidecar), not the main repo. + wikiGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo.WikiStorageRepo()) + if err != nil { + // Wiki repo doesn't exist yet — not an error, just no wiki. return nil, nil } - gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo) - if err != nil { - log.Error("findOrgWikiCommit: OpenRepository(%s): %v", dbRepo.FullName(), err) - return nil, nil + branch := dbRepo.DefaultWikiBranch + if branch == "" { + branch = "main" } - - commit, err := gitRepo.GetBranchCommit(dbRepo.DefaultBranch) + commit, err := wikiGitRepo.GetBranchCommit(branch) if err != nil { - log.Error("findOrgWikiCommit: GetBranchCommit(%s, %s): %v", dbRepo.FullName(), dbRepo.DefaultBranch, err) + log.Error("findOrgWikiCommit: GetBranchCommit wiki(%s, %s): %v", dbRepo.FullName(), branch, err) return nil, nil } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 9aacaa31a6..400c10f9d0 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -137,8 +137,8 @@ type PrepareOwnerHeaderResult struct { const ( RepoNameProfilePrivate = ".profile-private" RepoNameProfile = ".profile" - RepoNameWikiPublic = "wiki" - RepoNameWikiPrivate = "wiki-private" + RepoNameWikiPublic = ".profile" + RepoNameWikiPrivate = ".profile-private" ) func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) { @@ -209,11 +209,13 @@ func loadHeaderCount(ctx *context.Context) error { return nil } -// OrgWikiRepoExists checks whether a convention wiki repo exists and is non-empty. +// OrgWikiRepoExists checks whether a profile repo's wiki exists and has content. func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool { dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName) - if err != nil || dbRepo.IsEmpty { + if err != nil { return false } - return true + // Check if the wiki sidecar repo exists by trying to get its default branch. + _, err = gitrepo.GetDefaultBranch(ctx, dbRepo.WikiStorageRepo()) + return err == nil } diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index afef8baa82..2920ff2346 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -68,11 +68,11 @@
- +
-

Create repos named wiki (public) and/or wiki-private (members-only) under this organization.

+

Enable the wiki on .profile (public) and/or .profile-private (members-only) repos.

diff --git a/templates/org/wiki/view.tmpl b/templates/org/wiki/view.tmpl index 8faf6d85f0..d55bdbed9c 100644 --- a/templates/org/wiki/view.tmpl +++ b/templates/org/wiki/view.tmpl @@ -11,8 +11,8 @@ This organization doesn't have a wiki yet.

- Create a repository named wiki (public) or wiki-private (members-only) - with markdown files to get started. + Enable the wiki on the .profile (public) or .profile-private (members-only) + repository to get started.

{{else}} -- 2.52.0