Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Miller add7c0da4d feat: make metadata/manifest GET endpoint publicly accessible (#676)
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
PR RC Release / Build RC Release (pull_request) Failing after 54s
Universal: PR Check / Secret Scan (pull_request) Successful in 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 42s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Publish to Composer / Publish Package (release) Failing after 5s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Remove reqRepoReader auth requirement from GET /repos/{owner}/{repo}/metadata
and /manifest endpoints. PUT (update) still requires token + admin.
2026-06-21 10:18:07 -05:00
17 changed files with 61 additions and 976 deletions
+7 -43
View File
@@ -10,9 +10,9 @@
# VERSION: 05.00.00 # VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +=======================================================================+ # +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE | # | UNIVERSAL BUILD & RELEASE PIPELINE |
# +=======================================================================+ # +========================================================================+
# | | # | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | # | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | | # | |
@@ -21,7 +21,7 @@
# | dolibarr: mod*.class.php, update.txt, dev version reset | # | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream | # | generic: README-only, no update stream |
# | | # | |
# +=======================================================================+ # +========================================================================+
name: "Universal: Build & Release" name: "Universal: Build & Release"
@@ -51,7 +51,7 @@ permissions:
contents: write contents: write
jobs: jobs:
# ── PR Opened → Rename branch to RC and build RC release ───────────────────────── # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc: promote-rc:
name: Promote to RC name: Promote to RC
runs-on: release runs-on: release
@@ -149,7 +149,7 @@ jobs:
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ───────────────────────── # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release: release:
name: Build & Release Pipeline name: Build & Release Pipeline
runs-on: release runs-on: release
@@ -241,47 +241,11 @@ jobs:
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
PLATFORM="${{ steps.platform.outputs.platform }}" echo "tag=stable" >> "$GITHUB_OUTPUT"
if [[ "$PLATFORM" == joomla* ]]; then echo "release_tag=stable" >> "$GITHUB_OUTPUT"
echo "tag=stable" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
else
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "branch=main" >> "$GITHUB_OUTPUT" echo "branch=main" >> "$GITHUB_OUTPUT"
echo "Published version: ${VERSION}" echo "Published version: ${VERSION}"
- name: "Create semver tag for non-Joomla repos"
id: semver
if: |
steps.version.outputs.skip != 'true' &&
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}"
echo "Creating semver tag: ${SEMVER_TAG}"
# Create the git tag via API
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/tags" \
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "Created semver tag: ${SEMVER_TAG}"
elif [ "$HTTP_CODE" = "409" ]; then
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
else
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
fi
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
- name: Update release notes and promote changelog - name: Update release notes and promote changelog
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-16
View File
@@ -88,20 +88,8 @@ jobs:
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Check platform eligibility (Joomla only)
id: eligibility
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
fi
- name: Resolve metadata and bump version - name: Resolve metadata and bump version
id: meta id: meta
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
# Auto-detect stability from branch name on push, or use input on dispatch # Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then if [ "${{ github.event_name }}" = "push" ]; then
@@ -178,7 +166,6 @@ jobs:
- name: Create release - name: Create release
id: release id: release
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
@@ -189,7 +176,6 @@ jobs:
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md - name: Update release notes from CHANGELOG.md
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
@@ -226,7 +212,6 @@ jobs:
- name: Build package and upload - name: Build package and upload
id: package id: package
if: steps.eligibility.outputs.proceed == 'true'
run: | run: |
VERSION="${{ steps.meta.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
@@ -240,7 +225,6 @@ jobs:
# No need to build, commit, or sync updates.xml from workflows # No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)" - name: "Delete lesser pre-release channels (cascade)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-19
View File
@@ -10,25 +10,6 @@
- 13 seeded product tiers from base to enterprise - 13 seeded product tiers from base to enterprise
- DLID-gated update XML endpoint: GET /api/v1/licensing/updates/{product}.xml - DLID-gated update XML endpoint: GET /api/v1/licensing/updates/{product}.xml
- Profile repo fallback chain: .mokogitea > .profile > .github - Profile repo fallback chain: .mokogitea > .profile > .github
- Metadata/manifest GET endpoint publicly accessible without auth (#676)
- Org wiki: folder-based collapsible tree sidebar, _Sidebar.md overrides (#680)
- Wiki backlinks: "What links here" page showing all pages referencing current page (#669)
- Wiki wikilinks: [[Page Name]] and [[Page|Display Text]] syntax with red links for missing pages (#666)
- Required baseline issue statuses: Open and Closed are indestructible (is_required flag) (#681)
- Issue status API response includes is_required field
- Wiki recent changes page: cross-page edit activity with pagination (#670)
- Wiki page rename with automatic redirects via YAML frontmatter (#672)
### Fixed
- Metadata settings template 500 error: removed reference to deleted Version field
- Wiki recent changes: use commit.MessageTitle() instead of commit.Message()
- Wiki backlinks: proper URL encoding for subdirectory pages
- Wiki wikilinks: page existence lookup normalizes spaces and hyphens
- Issue statuses template: garbled em-dash character replaced
### Changed
- Issue status seed defaults: Open, In Progress, Waiting, In Review, Closed, Won't Fix
- Pre-release workflow: auto-bump skipped for non-Joomla repos (platform check)
## [06.19.00] --- 2026-06-20 ## [06.19.00] --- 2026-06-20
+6 -32
View File
@@ -22,7 +22,6 @@ type IssueStatusDef struct {
Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48" Color string `xorm:"VARCHAR(7)"` // hex color, e.g. "#e11d48"
Description string `xorm:"TEXT"` Description string `xorm:"TEXT"`
ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"` ClosesIssue bool `xorm:"NOT NULL DEFAULT false 'closes_issue'"`
IsRequired bool `xorm:"NOT NULL DEFAULT false 'is_required'"` // cannot be deleted
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"` SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"` IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
@@ -57,15 +56,14 @@ func GetIssueStatusDefsByOrg(ctx context.Context, orgID int64) ([]*IssueStatusDe
} }
// seedDefaultIssueStatuses creates the standard status presets for an org. // seedDefaultIssueStatuses creates the standard status presets for an org.
// Open and Closed are required (is_required=true) and cannot be deleted.
func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error { func seedDefaultIssueStatuses(ctx context.Context, orgID int64) error {
defaults := []*IssueStatusDef{ defaults := []*IssueStatusDef{
{OrgID: orgID, Name: "Open", Color: "#2563eb", Description: "New or active issue", ClosesIssue: false, IsRequired: true, SortOrder: 0, IsActive: true}, {OrgID: orgID, Name: "In Progress", Color: "#2563eb", Description: "Work is actively being done", SortOrder: 1, IsActive: true},
{OrgID: orgID, Name: "In Progress", Color: "#7c3aed", Description: "Work is actively being done", SortOrder: 1, IsActive: true}, {OrgID: orgID, Name: "Needs Info", Color: "#f59e0b", Description: "Waiting for more information", SortOrder: 2, IsActive: true},
{OrgID: orgID, Name: "Waiting", Color: "#f59e0b", Description: "Blocked or waiting for input", SortOrder: 2, IsActive: true}, {OrgID: orgID, Name: "Blocked", Color: "#dc2626", Description: "Cannot proceed due to dependency", SortOrder: 3, IsActive: true},
{OrgID: orgID, Name: "In Review", Color: "#0891b2", Description: "PR submitted, awaiting review", SortOrder: 3, IsActive: true}, {OrgID: orgID, Name: "Resolved", Color: "#16a34a", Description: "Fix implemented and verified", ClosesIssue: true, SortOrder: 4, IsActive: true},
{OrgID: orgID, Name: "Closed", Color: "#16a34a", Description: "Completed or resolved", ClosesIssue: true, IsRequired: true, SortOrder: 4, IsActive: true},
{OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true}, {OrgID: orgID, Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true, SortOrder: 5, IsActive: true},
{OrgID: orgID, Name: "Duplicate", Color: "#8b5cf6", Description: "Already tracked elsewhere", ClosesIssue: true, SortOrder: 6, IsActive: true},
} }
for _, d := range defaults { for _, d := range defaults {
if _, err := db.GetEngine(ctx).Insert(d); err != nil { if _, err := db.GetEngine(ctx).Insert(d); err != nil {
@@ -113,37 +111,13 @@ func UpdateIssueStatusDef(ctx context.Context, def *IssueStatusDef) error {
return err return err
} }
// ErrStatusRequired is returned when trying to delete a required status.
type ErrStatusRequired struct {
ID int64
Name string
}
func (e ErrStatusRequired) Error() string {
return "status is required and cannot be deleted"
}
// IsErrStatusRequired checks if an error is ErrStatusRequired.
func IsErrStatusRequired(err error) bool {
_, ok := err.(ErrStatusRequired)
return ok
}
// DeleteIssueStatusDef deletes a status definition and clears references on issues. // DeleteIssueStatusDef deletes a status definition and clears references on issues.
// Returns ErrStatusRequired if the status is marked as required.
func DeleteIssueStatusDef(ctx context.Context, id int64) error { func DeleteIssueStatusDef(ctx context.Context, id int64) error {
def, err := GetIssueStatusDefByID(ctx, id)
if err != nil {
return err
}
if def.IsRequired {
return ErrStatusRequired{ID: def.ID, Name: def.Name}
}
// Clear status_id on all issues that reference this definition // Clear status_id on all issues that reference this definition
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil { if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET status_id = 0 WHERE status_id = ?", id); err != nil {
return err return err
} }
_, err = db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef)) _, err := db.GetEngine(ctx).ID(id).Delete(new(IssueStatusDef))
return err return err
} }
-1
View File
@@ -165,7 +165,6 @@ type IssueStatusDef struct {
Color string `json:"color"` Color string `json:"color"`
Description string `json:"description"` Description string `json:"description"`
ClosesIssue bool `json:"closes_issue"` ClosesIssue bool `json:"closes_issue"`
IsRequired bool `json:"is_required"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
} }
-26
View File
@@ -11,19 +11,6 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
) )
// checkOrgVisibility returns true if the current user can view org metadata.
// Public orgs are visible to everyone. Private/limited orgs require authentication.
func checkOrgVisibility(ctx *context.APIContext) bool {
if ctx.Org.Organization.Visibility == api.VisibleTypePublic {
return true
}
if ctx.Doer == nil {
ctx.APIErrorNotFound()
return false
}
return true
}
// ListIssueStatuses returns active issue status definitions for an org. // ListIssueStatuses returns active issue status definitions for an org.
func ListIssueStatuses(ctx *context.APIContext) { func ListIssueStatuses(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses // swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
@@ -47,10 +34,6 @@ func ListIssueStatuses(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if !checkOrgVisibility(ctx) {
return
}
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID) defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -64,7 +47,6 @@ func ListIssueStatuses(ctx *context.APIContext) {
Color: d.Color, Color: d.Color,
Description: d.Description, Description: d.Description,
ClosesIssue: d.ClosesIssue, ClosesIssue: d.ClosesIssue,
IsRequired: d.IsRequired,
SortOrder: d.SortOrder, SortOrder: d.SortOrder,
}) })
} }
@@ -94,10 +76,6 @@ func ListIssuePriorities(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if !checkOrgVisibility(ctx) {
return
}
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID) defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -140,10 +118,6 @@ func ListIssueTypes(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if !checkOrgVisibility(ctx) {
return
}
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID) defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
-5
View File
@@ -103,11 +103,6 @@ func SettingsIssueStatusesDeletePost(ctx *context.Context) {
} }
if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil { if err := issues_model.DeleteIssueStatusDef(ctx, id); err != nil {
if issues_model.IsErrStatusRequired(err) {
ctx.Flash.Error("Cannot delete required status: " + def.Name)
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
return
}
ctx.ServerError("DeleteIssueStatusDef", err) ctx.ServerError("DeleteIssueStatusDef", err)
return return
} }
+25 -73
View File
@@ -29,14 +29,6 @@ type OrgWikiPage struct {
SubURL string SubURL string
} }
// OrgWikiTreeNode represents a node in the org wiki folder tree for sidebar navigation.
type OrgWikiTreeNode struct {
Name string
SubURL string
IsDir bool
Children []*OrgWikiTreeNode
}
// Wiki renders the org wiki tab. // Wiki renders the org wiki tab.
func Wiki(ctx *context.Context) { func Wiki(ctx *context.Context) {
org := ctx.Org.Organization org := ctx.Org.Organization
@@ -79,9 +71,31 @@ func Wiki(ctx *context.Context) {
} }
ctx.Data["WikiRepoLink"] = wikiRepo.Link() ctx.Data["WikiRepoLink"] = wikiRepo.Link()
// Build folder tree for sidebar navigation. // Build page list from repo root.
wikiTree := buildOrgWikiTree(commit) entries, err := commit.ListEntries()
ctx.Data["WikiTree"] = wikiTree if err != nil {
ctx.ServerError("ListEntries", err)
return
}
pages := make([]OrgWikiPage, 0, len(entries))
for _, entry := range entries {
if !entry.IsRegular() {
continue
}
name := entry.Name()
if !isMarkdownFile(name) {
continue
}
displayName := strings.TrimSuffix(name, path.Ext(name))
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
continue
}
pages = append(pages, OrgWikiPage{
Name: displayName,
SubURL: displayName,
})
}
ctx.Data["Pages"] = pages
// Determine which page to render. // Determine which page to render.
pageName := ctx.PathParamRaw("*") pageName := ctx.PathParamRaw("*")
@@ -143,68 +157,6 @@ func Wiki(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplOrgWiki) ctx.HTML(http.StatusOK, tplOrgWiki)
} }
// buildOrgWikiTree builds a hierarchical folder tree from the org wiki git repo.
// Shows up to 2 levels deep (folders and their immediate children).
func buildOrgWikiTree(commit *git.Commit) []*OrgWikiTreeNode {
if commit == nil {
return nil
}
entries, err := commit.ListEntries()
if err != nil {
return nil
}
var topLevel []*OrgWikiTreeNode
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
node := &OrgWikiTreeNode{
Name: name,
SubURL: name,
IsDir: true,
}
// List children of this directory (1 level deep).
subTree := entry.Tree()
if subTree != nil {
children, _ := subTree.ListEntries()
for _, child := range children {
childName := child.Name()
if child.IsDir() {
node.Children = append(node.Children, &OrgWikiTreeNode{
Name: childName,
SubURL: name + "/" + childName,
IsDir: true,
})
} else if isMarkdownFile(childName) {
displayName := strings.TrimSuffix(childName, path.Ext(childName))
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
continue
}
node.Children = append(node.Children, &OrgWikiTreeNode{
Name: displayName,
SubURL: name + "/" + displayName,
IsDir: false,
})
}
}
}
topLevel = append(topLevel, node)
} else if isMarkdownFile(name) {
displayName := strings.TrimSuffix(name, path.Ext(name))
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
continue
}
topLevel = append(topLevel, &OrgWikiTreeNode{
Name: displayName,
SubURL: displayName,
IsDir: false,
})
}
}
return topLevel
}
// findOrgWikiCommit locates the profile repo's wiki 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. .mokogitea.wiki.git). // The org wiki lives in the .wiki.git sidecar of the profile repo (e.g. .mokogitea.wiki.git).
// Tries fallback repo names (.profile, .github) if the primary doesn't exist. // Tries fallback repo names (.profile, .github) if the primary doesn't exist.
+6 -540
View File
@@ -6,14 +6,11 @@ package repo
import ( import (
"bytes" "bytes"
"html"
"html/template" "html/template"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"regexp"
"strconv"
"strings" "strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper" "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
@@ -41,14 +38,11 @@ import (
) )
const ( const (
tplWikiStart templates.TplName = "repo/wiki/start" tplWikiStart templates.TplName = "repo/wiki/start"
tplWikiView templates.TplName = "repo/wiki/view" tplWikiView templates.TplName = "repo/wiki/view"
tplWikiRevision templates.TplName = "repo/wiki/revision" tplWikiRevision templates.TplName = "repo/wiki/revision"
tplWikiNew templates.TplName = "repo/wiki/new" tplWikiNew templates.TplName = "repo/wiki/new"
tplWikiPages templates.TplName = "repo/wiki/pages" tplWikiPages templates.TplName = "repo/wiki/pages"
tplWikiBacklinks templates.TplName = "repo/wiki/backlinks"
tplWikiRecentChanges templates.TplName = "repo/wiki/recent"
tplWikiDiff templates.TplName = "repo/wiki/diff"
) )
// MustEnableWiki check if wiki is enabled, if external then redirect // MustEnableWiki check if wiki is enabled, if external then redirect
@@ -307,14 +301,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return nil, nil return nil, nil
} }
// Check for redirect frontmatter: ---\nredirect: target\n---
if target := extractWikiRedirect(data); target != "" {
redirectURL := ctx.Repo.RepoLink + "/wiki/" + wiki_service.WebPathToURLPath(wiki_service.WebPath(target))
ctx.Flash.Info("Redirected from " + displayName)
ctx.Redirect(redirectURL)
return nil, nil
}
rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository) rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository)
renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) { renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) {
@@ -335,9 +321,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return escaped, output, err return escaped, output, err
} }
// Preprocess wikilinks: [[Page Name]] → HTML links before markdown rendering.
data = preprocessWikilinks(data, commit, ctx.Repo.RepoLink+"/wiki/")
ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data) ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data)
if err != nil { if err != nil {
ctx.ServerError("Render", err) ctx.ServerError("Render", err)
@@ -374,15 +357,10 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
} }
} }
// get commit count and last commit for wiki revisions // get commit count - wiki revisions
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename) commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount ctx.Data["CommitCount"] = commitsCount
// Pass last commit ID for diff link
if lastCommit, err := wikiGitRepo.GetCommitByPath(pageFilename); err == nil && lastCommit != nil {
ctx.Data["LastCommitID"] = lastCommit.ID.String()
}
return wikiGitRepo, entry return wikiGitRepo, entry
} }
@@ -526,15 +504,6 @@ func Wiki(ctx *context.Context) {
case "_revision": case "_revision":
WikiRevision(ctx) WikiRevision(ctx)
return return
case "_backlinks":
WikiBacklinks(ctx)
return
case "_recent":
WikiRecentChanges(ctx)
return
case "_diff":
WikiDiff(ctx)
return
case "_edit": case "_edit":
if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) { if !ctx.Repo.Permission.CanWrite(unit.TypeWiki) {
ctx.NotFound(nil) ctx.NotFound(nil)
@@ -592,376 +561,6 @@ func Wiki(ctx *context.Context) {
} }
// WikiRevision renders file revision list of wiki page // WikiRevision renders file revision list of wiki page
// WikiBacklinks shows all wiki pages that link to the current page.
func WikiBacklinks(ctx *context.Context) {
ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
if !repo_service.HasWiki(ctx, ctx.Repo.Repository) {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
return
}
_, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("findWikiRepoCommit", err)
}
return
}
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
}
_, displayName := wiki_service.WebPathToUserTitle(pageName)
ctx.Data["Title"] = "What links here: " + displayName
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
ctx.Data["title"] = displayName
searchTerms := []string{
string(pageName), // raw path
wiki_service.WebPathToURLPath(wiki_service.WebPath(pageName)), // URL-encoded path
displayName, // display name
}
type BacklinkResult struct {
PageName string
PageURL string
Context string
}
seen := make(map[string]bool)
var backlinks []BacklinkResult
entries, _ := commit.ListEntries()
for _, entry := range entries {
if !entry.IsRegular() || !strings.HasSuffix(entry.Name(), ".md") {
continue
}
entryName := strings.TrimSuffix(entry.Name(), ".md")
if entryName == string(pageName) || entryName == "_Sidebar" || entryName == "_Footer" {
continue
}
blob := entry.Blob()
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err != nil {
continue
}
for _, term := range searchTerms {
if strings.Contains(content, term) && !seen[entryName] {
seen[entryName] = true
// Extract a line of context around the match
contextLine := ""
for _, line := range strings.Split(content, "\n") {
if strings.Contains(line, term) {
contextLine = strings.TrimSpace(line)
if len(contextLine) > 200 {
contextLine = contextLine[:200] + "..."
}
break
}
}
wpName, _ := wiki_service.GitPathToWebPath(entry.Name())
_, linkDisplay := wiki_service.WebPathToUserTitle(wpName)
backlinks = append(backlinks, BacklinkResult{
PageName: linkDisplay,
PageURL: string(wpName),
Context: contextLine,
})
break
}
}
}
// Also search subdirectories (1 level deep)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
subTree := entry.Tree()
if subTree == nil {
continue
}
children, _ := subTree.ListEntries()
for _, child := range children {
if !child.IsRegular() || !strings.HasSuffix(child.Name(), ".md") {
continue
}
fullPath := entry.Name() + "/" + strings.TrimSuffix(child.Name(), ".md")
if fullPath == string(pageName) || seen[fullPath] {
continue
}
childName := strings.TrimSuffix(child.Name(), ".md")
if childName == "_Sidebar" || childName == "_Footer" {
continue
}
blob := child.Blob()
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err != nil {
continue
}
for _, term := range searchTerms {
if strings.Contains(content, term) && !seen[fullPath] {
seen[fullPath] = true
contextLine := ""
for _, line := range strings.Split(content, "\n") {
if strings.Contains(line, term) {
contextLine = strings.TrimSpace(line)
if len(contextLine) > 200 {
contextLine = contextLine[:200] + "..."
}
break
}
}
wpChild, _ := wiki_service.GitPathToWebPath(child.Name())
_, childDisplay := wiki_service.WebPathToUserTitle(wpChild)
backlinks = append(backlinks, BacklinkResult{
PageName: childDisplay,
PageURL: entry.Name() + "/" + string(wpChild),
Context: contextLine,
})
break
}
}
}
}
ctx.Data["Backlinks"] = backlinks
ctx.Data["BacklinkCount"] = len(backlinks)
ctx.HTML(http.StatusOK, tplWikiBacklinks)
}
// WikiRecentChanges shows all recent wiki edits across all pages.
func WikiRecentChanges(ctx *context.Context) {
ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
ctx.Data["Title"] = "Recent changes"
if !repo_service.HasWiki(ctx, ctx.Repo.Repository) {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
return
}
wikiGitRepo, _, err := findWikiRepoCommit(ctx)
if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("findWikiRepoCommit", err)
}
return
}
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
branch := ctx.Repo.Repository.DefaultWikiBranch
if branch == "" {
branch = "main"
}
// Get all commits (no file filter = all wiki changes).
commits, err := wikiGitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: branch,
Page: page,
},
)
if err != nil {
ctx.ServerError("CommitsByFileAndRange", err)
return
}
type RecentChange struct {
PageName string
PageURL string
Author string
Message string
When timeutil.TimeStamp
SHA string
}
var changes []RecentChange
for _, commit := range commits {
// Get files changed by comparing to parent
var changedFiles []string
parents := commit.Parents
if len(parents) > 0 {
changedFiles, _ = commit.GetFilesChangedSinceCommit(parents[0].String())
}
pageNames := make([]string, 0)
for _, f := range changedFiles {
if strings.HasSuffix(f, ".md") {
name := strings.TrimSuffix(f, ".md")
if name != "_Sidebar" && name != "_Footer" {
pageNames = append(pageNames, name)
}
}
}
displayPage := ""
pageURL := ""
if len(pageNames) == 1 {
displayPage = pageNames[0]
pageURL = strings.ReplaceAll(pageNames[0], " ", "-")
} else if len(pageNames) > 1 {
displayPage = pageNames[0] + " (+" + strconv.Itoa(len(pageNames)-1) + " more)"
pageURL = strings.ReplaceAll(pageNames[0], " ", "-")
} else if len(changedFiles) > 0 {
// Non-markdown files changed (images, etc.)
displayPage = changedFiles[0]
}
msg := commit.MessageTitle()
changes = append(changes, RecentChange{
PageName: displayPage,
PageURL: pageURL,
Author: commit.Author.Name,
Message: msg,
When: timeutil.TimeStamp(commit.Author.When.Unix()),
SHA: commit.ID.String()[:10],
})
}
ctx.Data["RecentChanges"] = changes
ctx.Data["CurrentPage"] = page
ctx.Data["HasNextPage"] = len(commits) >= setting.Git.CommitsRangeSize
ctx.Data["HasPrevPage"] = page > 1
ctx.HTML(http.StatusOK, tplWikiRecentChanges)
}
// WikiDiff shows the diff between a commit and its parent for a wiki page.
func WikiDiff(ctx *context.Context) {
ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
if !repo_service.HasWiki(ctx, ctx.Repo.Repository) {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
return
}
wikiGitRepo, _, err := findWikiRepoCommit(ctx)
if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("findWikiRepoCommit", err)
}
return
}
commitID := ctx.FormString("commit")
if commitID == "" {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/")
return
}
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
}
_, displayName := wiki_service.WebPathToUserTitle(pageName)
ctx.Data["Title"] = "Diff: " + displayName
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
ctx.Data["title"] = displayName
commit, err := wikiGitRepo.GetCommit(commitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
ctx.Data["CommitID"] = commit.ID.String()[:10]
ctx.Data["CommitMessage"] = commit.MessageTitle()
ctx.Data["CommitAuthor"] = commit.Author.Name
ctx.Data["CommitWhen"] = timeutil.TimeStamp(commit.Author.When.Unix())
// Get the file path for this wiki page
wikiPath := string(pageName) + ".md"
// Get content at this commit
blob, _ := commit.GetBlobByPath(wikiPath)
newContent := ""
if blob != nil {
newContent, _ = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
}
// Get content at parent commit
oldContent := ""
if len(commit.Parents) > 0 {
parentCommit, err := wikiGitRepo.GetCommit(commit.Parents[0].String())
if err == nil {
parentBlob, _ := parentCommit.GetBlobByPath(wikiPath)
if parentBlob != nil {
oldContent, _ = parentBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
}
}
}
// Build a simple line-by-line diff
type DiffLine struct {
Type string // "add", "del", "ctx"
Content string
OldNum int
NewNum int
}
oldLines := strings.Split(oldContent, "\n")
newLines := strings.Split(newContent, "\n")
var diffLines []DiffLine
// Simple diff: show removed lines then added lines
// For a proper diff we'd use a real diff algorithm, but this gives useful output
oldSet := make(map[string]bool)
newSet := make(map[string]bool)
for _, l := range oldLines {
oldSet[l] = true
}
for _, l := range newLines {
newSet[l] = true
}
oldNum := 0
newNum := 0
maxLines := len(oldLines)
if len(newLines) > maxLines {
maxLines = len(newLines)
}
// Walk through both files showing context, deletions, and additions
for i := 0; i < maxLines; i++ {
if i < len(oldLines) && i < len(newLines) && oldLines[i] == newLines[i] {
oldNum++
newNum++
diffLines = append(diffLines, DiffLine{Type: "ctx", Content: oldLines[i], OldNum: oldNum, NewNum: newNum})
} else {
if i < len(oldLines) {
oldNum++
diffLines = append(diffLines, DiffLine{Type: "del", Content: oldLines[i], OldNum: oldNum})
}
if i < len(newLines) {
newNum++
diffLines = append(diffLines, DiffLine{Type: "add", Content: newLines[i], NewNum: newNum})
}
}
}
ctx.Data["DiffLines"] = diffLines
ctx.Data["HasDiff"] = oldContent != newContent
ctx.Data["IsNewPage"] = oldContent == ""
ctx.Data["IsDeletedPage"] = newContent == ""
ctx.HTML(http.StatusOK, tplWikiDiff)
}
func WikiRevision(ctx *context.Context) { func WikiRevision(ctx *context.Context) {
ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
@@ -1191,13 +790,6 @@ func EditWikiPost(ctx *context.Context) {
return return
} }
// If page was renamed, create a redirect at the old path.
if oldWikiName != newWikiName {
_, newDisplay := wiki_service.WebPathToUserTitle(newWikiName)
redirectContent := "---\nredirect: " + string(newWikiName) + "\n---\nThis page has moved to [" + newDisplay + "](" + wiki_service.WebPathToURLPath(newWikiName) + ").\n"
_ = wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, redirectContent, "redirect: "+string(oldWikiName)+" → "+string(newWikiName))
}
notify_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message) notify_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message)
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.WebPathToURLPath(newWikiName)) ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.WebPathToURLPath(newWikiName))
@@ -1237,132 +829,6 @@ func buildWikiBreadcrumbs(pageName wiki_service.WebPath) []WikiBreadcrumb {
return crumbs return crumbs
} }
// extractWikiRedirect checks if page content starts with YAML frontmatter containing
// a "redirect:" field. Returns the target page path or empty string.
func extractWikiRedirect(data []byte) string {
content := string(data)
if !strings.HasPrefix(content, "---\n") {
return ""
}
endIdx := strings.Index(content[4:], "\n---")
if endIdx < 0 {
return ""
}
frontmatter := content[4 : 4+endIdx]
for _, line := range strings.Split(frontmatter, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "redirect:") {
target := strings.TrimSpace(strings.TrimPrefix(line, "redirect:"))
return target
}
}
return ""
}
// wikilinkPattern matches [[Page Name]] and [[Page Name|Display Text]] syntax.
var wikilinkPattern = regexp.MustCompile(`\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]`)
// preprocessWikilinks replaces [[Page Name]] syntax with HTML links before markdown rendering.
// Existing pages get normal links; non-existent pages get red "new page" links.
func preprocessWikilinks(data []byte, commit *git.Commit, wikiBaseURL string) []byte {
if commit == nil {
return data
}
// Build a set of existing page paths for quick lookup.
existingPages := make(map[string]bool)
collectWikiPages(commit, "", existingPages)
result := wikilinkPattern.ReplaceAllFunc(data, func(match []byte) []byte {
parts := wikilinkPattern.FindSubmatch(match)
if len(parts) < 2 {
return match
}
pagePath := strings.TrimSpace(string(parts[1]))
displayText := pagePath
if len(parts) >= 3 && len(parts[2]) > 0 {
displayText = strings.TrimSpace(string(parts[2]))
}
// Handle anchor links: [[#Section]] or [[Page#Section]]
anchor := ""
if idx := strings.Index(pagePath, "#"); idx >= 0 {
anchor = pagePath[idx:]
pagePath = pagePath[:idx]
}
// Pure anchor link on current page
if pagePath == "" && anchor != "" {
return []byte(`<a href="` + html.EscapeString(anchor) + `">` + html.EscapeString(displayText) + `</a>`)
}
// Normalize the page path for lookup
lookupKey := strings.ReplaceAll(pagePath, " ", "-")
// Check if page exists (try with and without folder prefix)
pageExists := existingPages[lookupKey] ||
existingPages[strings.ToLower(lookupKey)]
escapedURL := html.EscapeString(wikiBaseURL + url.PathEscape(lookupKey) + anchor)
escapedText := html.EscapeString(displayText)
if pageExists {
return []byte(`<a href="` + escapedURL + `">` + escapedText + `</a>`)
}
// Red link for non-existent pages — links to create page
createURL := html.EscapeString(wikiBaseURL + "?action=_new&title=" + url.QueryEscape(pagePath))
return []byte(`<a href="` + createURL + `" class="wiki-link-new" title="Page does not exist (click to create)">` + escapedText + `</a>`)
})
return result
}
// collectWikiPages builds a set of all wiki page paths from the commit tree.
// Stores both raw names and hyphen-normalized names for flexible lookup.
func collectWikiPages(commit *git.Commit, prefix string, pages map[string]bool) {
entries, err := commit.ListEntries()
if err != nil {
return
}
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
subTree := entry.Tree()
if subTree == nil {
continue
}
children, _ := subTree.ListEntries()
for _, child := range children {
childName := child.Name()
if child.IsRegular() && strings.HasSuffix(childName, ".md") {
pageName := strings.TrimSuffix(childName, ".md")
fullPath := name + "/" + pageName
registerWikiPage(pages, fullPath)
}
}
} else if strings.HasSuffix(name, ".md") {
pageName := strings.TrimSuffix(name, ".md")
if prefix != "" {
pageName = prefix + "/" + pageName
}
registerWikiPage(pages, pageName)
}
}
}
// registerWikiPage adds a page name to the lookup map with multiple normalizations.
func registerWikiPage(pages map[string]bool, name string) {
pages[name] = true
pages[strings.ToLower(name)] = true
// Also store hyphen-normalized version (spaces → hyphens)
hyphenized := strings.ReplaceAll(name, " ", "-")
if hyphenized != name {
pages[hyphenized] = true
pages[strings.ToLower(hyphenized)] = true
}
}
// buildWikiTree builds a hierarchical folder tree from the wiki git repo. // buildWikiTree builds a hierarchical folder tree from the wiki git repo.
func buildWikiTree(commit *git.Commit) []*WikiTreeNode { func buildWikiTree(commit *git.Commit) []*WikiTreeNode {
if commit == nil { if commit == nil {
@@ -28,7 +28,6 @@
</td> </td>
<td> <td>
<strong>{{.Name}}</strong> <strong>{{.Name}}</strong>
{{if .IsRequired}}<span class="ui mini blue label" title="Required status - cannot be deleted">{{svg "octicon-lock" 10}} required</span>{{end}}
{{if not .IsActive}}<span class="ui mini grey label">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}} {{if not .IsActive}}<span class="ui mini grey label">{{ctx.Locale.Tr "org.settings.issue_status_inactive"}}</span>{{end}}
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}} {{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td> </td>
@@ -41,14 +40,10 @@
</td> </td>
<td>{{.SortOrder}}</td> <td>{{.SortOrder}}</td>
<td class="tw-text-right"> <td class="tw-text-right">
{{if .IsRequired}}
<span class="ui tiny icon button disabled" title="Required - cannot be deleted">{{svg "octicon-lock" 14}}</span>
{{else}}
<form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.ID}}/delete" class="tw-inline"> <form method="post" action="{{$.OrgLink}}/settings/issue-statuses/{{.ID}}/delete" class="tw-inline">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button> <button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
</form> </form>
{{end}}
</td> </td>
</tr> </tr>
{{end}} {{end}}
+12 -37
View File
@@ -11,7 +11,7 @@
This organization doesn't have a wiki yet. This organization doesn't have a wiki yet.
</div> </div>
<p class="tw-text-center"> <p class="tw-text-center">
Enable the wiki on the <code>.mokogitea</code> (public) or <code>.mokogitea-private</code> (members-only) Enable the wiki on the <code>.profile</code> (public) or <code>.profile-private</code> (members-only)
repository to get started. repository to get started.
</p> </p>
</div> </div>
@@ -47,59 +47,34 @@
<p>The page "{{.CurrentPage}}" does not exist in this wiki.</p> <p>The page "{{.CurrentPage}}" does not exist in this wiki.</p>
</div> </div>
</div> </div>
{{if .WikiTree}} {{if .Pages}}
<h4>Available pages:</h4> <h4>Available pages:</h4>
<ul> <ul>
{{range .WikiTree}} {{range .Pages}}
{{if .IsDir}} <li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
{{range .Children}}
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
{{end}}
{{else}}
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
{{end}}
{{end}} {{end}}
</ul> </ul>
{{end}} {{end}}
</div> </div>
{{else}} {{else}}
<div class="wiki-content-parts"> <div class="wiki-content-parts">
<div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}"> <div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .Pages}}with-sidebar{{end}}">
{{.WikiContent}} {{.WikiContent}}
</div> </div>
{{if or .WikiSidebarHTML .WikiTree}} {{if or .WikiSidebarHTML .Pages}}
<div class="render-content markup wiki-content-sidebar"> <div class="render-content markup wiki-content-sidebar">
{{if .WikiSidebarHTML}} {{if .WikiSidebarHTML}}
{{.WikiSidebarHTML}} {{.WikiSidebarHTML}}
{{else if .WikiTree}} <div class="ui divider"></div>
{{end}}
{{if .Pages}}
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong> <strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
<ul class="wiki-tree-list"> <ul class="wiki-tree-list">
{{range .WikiTree}} {{range .Pages}}
<li> <li>
{{if .IsDir}} {{svg "octicon-file" 14}}
<details open> <a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
<summary>{{svg "octicon-file-directory" 14}} <strong>{{.Name}}</strong></summary>
{{if .Children}}
<ul>
{{range .Children}}
<li>
{{if .IsDir}}
{{svg "octicon-file-directory" 14}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li>
{{end}}
</ul>
{{end}}
</details>
{{else}}
{{svg "octicon-file" 14}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
{{end}}
</li> </li>
{{end}} {{end}}
</ul> </ul>
+5 -1
View File
@@ -21,7 +21,11 @@
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization"> <input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div> </div>
</div> </div>
<div class="three fields"> <div class="four fields">
<div class="field">
<label>Version</label>
<input name="version" value="{{.Manifest.Version}}" placeholder="e.g. 06.00.00">
</div>
<div class="field"> <div class="field">
<label>Version Prefix</label> <label>Version Prefix</label>
<input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko."> <input name="version_prefix" value="{{.Manifest.VersionPrefix}}" placeholder="e.g. v1.26.1-moko.">
-42
View File
@@ -1,42 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row">
<div class="tw-flex tw-items-center tw-gap-2">
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}">
{{svg "octicon-arrow-left" 14}} Back to {{.title}}
</a>
</div>
</div>
<h2>{{svg "octicon-cross-reference" 20}} What links here: {{.title}}</h2>
{{if .Backlinks}}
<div class="ui relaxed divided list">
{{range .Backlinks}}
<div class="item">
<div class="content">
<a class="header" href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
{{if .Context}}
<div class="description">
<code class="tw-text-sm">{{.Context}}</code>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
<p class="tw-mt-4 text grey">{{.BacklinkCount}} {{if eq .BacklinkCount 1}}page{{else}}pages{{end}} linking here.</p>
{{else}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-unlink" 48}}
<br>
No pages link to "{{.title}}"
</div>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
-51
View File
@@ -1,51 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
<div class="tw-flex-1">
<a href="{{.RepoLink}}/wiki/{{.PageURL}}">{{svg "octicon-arrow-left" 14}} {{.title}}</a>
&middot;
<a href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision">Revision history</a>
</div>
</div>
<div class="ui segment">
<h3>{{svg "octicon-diff" 20}} Changes in <code>{{.CommitID}}</code></h3>
<p>
<strong>{{.CommitAuthor}}</strong> &mdash; {{.CommitMessage}}
<br>
<small class="text grey">{{DateUtils.TimeSince .CommitWhen}}</small>
</p>
{{if .IsNewPage}}
<div class="ui info message">New page created</div>
{{else if .IsDeletedPage}}
<div class="ui warning message">Page deleted</div>
{{else if not .HasDiff}}
<div class="ui info message">No content changes in this revision</div>
{{end}}
{{if .HasDiff}}
<div class="diff-file-box" style="overflow-x: auto;">
<table class="chroma" style="width: 100%; border-collapse: collapse; font-family: monospace; font-size: 13px;">
{{range .DiffLines}}
<tr class="{{if eq .Type "add"}}diff-line-add{{else if eq .Type "del"}}diff-line-del{{else}}diff-line-context{{end}}">
<td style="width: 40px; text-align: right; padding: 0 8px; color: #999; user-select: none; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #f6f8fa;{{end}}">
{{if .OldNum}}{{.OldNum}}{{end}}
</td>
<td style="width: 40px; text-align: right; padding: 0 8px; color: #999; user-select: none; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #f6f8fa;{{end}}">
{{if .NewNum}}{{.NewNum}}{{end}}
</td>
<td style="padding: 0 8px; white-space: pre-wrap; word-break: break-all; {{if eq .Type "add"}}background: #e6ffec;{{else if eq .Type "del"}}background: #ffebe9;{{else}}background: #fff;{{end}}">
{{if eq .Type "add"}}+{{else if eq .Type "del"}}-{{else}} {{end}} {{.Content}}
</td>
</tr>
{{end}}
</table>
</div>
{{end}}
</div>
</div>
</div>
{{template "base/footer" .}}
-72
View File
@@ -1,72 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki">
{{template "repo/header" .}}
<div class="ui container">
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
<div class="tw-flex-1">
<h2>{{svg "octicon-history" 20}} Recent changes</h2>
</div>
<a class="ui small button" href="{{.RepoLink}}/wiki/">
{{svg "octicon-arrow-left" 14}} Back to wiki
</a>
</div>
{{if .RecentChanges}}
<table class="ui compact table">
<thead>
<tr>
<th>Page</th>
<th>Author</th>
<th>Edit summary</th>
<th>When</th>
<th>Commit</th>
</tr>
</thead>
<tbody>
{{range .RecentChanges}}
<tr>
<td>
{{if .PageURL}}
{{svg "octicon-file" 14}}
<a href="{{$.RepoLink}}/wiki/{{.PageURL}}">{{.PageName}}</a>
{{else if .PageName}}
{{svg "octicon-file" 14}} {{.PageName}}
{{else}}
<span class="text grey">—</span>
{{end}}
</td>
<td>{{.Author}}</td>
<td class="gt-ellipsis" style="max-width: 400px;">{{.Message}}</td>
<td>{{DateUtils.TimeSince .When}}</td>
<td><code class="tw-text-xs">{{.SHA}}</code></td>
</tr>
{{end}}
</tbody>
</table>
<div class="tw-flex tw-justify-between tw-mt-4">
{{if .HasPrevPage}}
<a class="ui small button" href="{{.RepoLink}}/wiki/?action=_recent&page={{Eval .CurrentPage "-" 1}}">
{{svg "octicon-chevron-left" 14}} Newer
</a>
{{else}}
<span></span>
{{end}}
{{if .HasNextPage}}
<a class="ui small button" href="{{.RepoLink}}/wiki/?action=_recent&page={{Eval .CurrentPage "+" 1}}">
Older {{svg "octicon-chevron-right" 14}}
</a>
{{end}}
</div>
{{else}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-history" 48}}
<br>
No recent changes
</div>
</div>
{{end}}
</div>
</div>
{{template "base/footer" .}}
-3
View File
@@ -20,7 +20,6 @@
</div> </div>
<div class="scrolling menu"> <div class="scrolling menu">
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_pages">{{ctx.Locale.Tr "repo.wiki.pages"}}</a> <a class="item muted" href="{{.RepoLink}}/wiki/?action=_pages">{{ctx.Locale.Tr "repo.wiki.pages"}}</a>
<a class="item muted" href="{{.RepoLink}}/wiki/?action=_recent">{{svg "octicon-history" 14}} Recent changes</a>
<div class="divider"></div> <div class="divider"></div>
{{range .Pages}} {{range .Pages}}
<a class="item {{if eq $.Title .Name}}selected{{end}}" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a> <a class="item {{if eq $.Title .Name}}selected{{end}}" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
@@ -35,8 +34,6 @@
<div class="flex-text-block tw-flex-wrap tw-justify-end"> <div class="flex-text-block tw-flex-wrap tw-justify-end">
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]"> <div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
<a class="ui basic button tw-px-3 tw-gap-3" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a> <a class="ui basic button tw-px-3 tw-gap-3" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
<a class="ui basic button tw-px-3" title="What links here" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_backlinks">{{svg "octicon-cross-reference"}}</a>
{{if .LastCommitID}}<a class="ui basic button tw-px-3" title="View last change" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_diff&commit={{.LastCommitID}}">{{svg "octicon-diff"}}</a>{{end}}
<div class="tw-flex-1 gt-ellipsis"> <div class="tw-flex-1 gt-ellipsis">
{{$title}} {{$title}}
<div class="ui sub header gt-ellipsis"> <div class="ui sub header gt-ellipsis">
-10
View File
@@ -86,13 +86,3 @@
max-width: unset; max-width: unset;
} }
} }
/* Wikilinks: red links for non-existent pages */
.wiki .wiki-link-new {
color: var(--color-red);
}
.wiki .wiki-link-new:hover {
color: var(--color-red);
text-decoration: underline;
}