Wiki title prefix policy

This commit is contained in:
2026-05-16 23:58:46 +02:00
parent d34a640efe
commit faf1459dba
6 changed files with 329 additions and 92 deletions
+96 -18
View File
@@ -33,20 +33,21 @@ const (
)
type DeployWikiOptions struct {
SourceDir string
Endpoint string
Token string
Version string
Namespaces []string
CategoryIDs map[string]int
ManifestPath string
DryRun bool
Force bool
AllowCreates bool
StalePolicy string
Username string
Password string
NotesDelimiter string
SourceDir string
Endpoint string
Token string
Version string
Namespaces []string
CategoryIDs map[string]int
ManifestPath string
DryRun bool
Force bool
AllowCreates bool
StalePolicy string
Username string
Password string
NotesDelimiter string
TitlePrefixMinLength int
}
type deployResult struct {
@@ -57,6 +58,7 @@ type deployResult struct {
Archived int
Skipped int
Drifted int
Renamed int
Manifest string
}
@@ -78,6 +80,7 @@ type wikiDeployManifestPage struct {
LastSeenHash string `json:"last_seen_hash,omitempty"`
ArchivedHash string `json:"archived_hash,omitempty"`
Title string `json:"title,omitempty"`
TopicTitle string `json:"topic_title,omitempty"`
Namespace string `json:"namespace,omitempty"`
Stale bool `json:"stale,omitempty"`
TID int `json:"tid,omitempty"`
@@ -91,6 +94,7 @@ type wikiDeployPlan struct {
Action string
Content string
RemoteHash string
Title string
}
type wikiNamespacesDocument struct {
@@ -128,6 +132,9 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
if opts.StalePolicy != "" && opts.StalePolicy != "report" && opts.StalePolicy != "archive" {
return deployResult{}, fmt.Errorf("wiki stale policy %q is not supported", opts.StalePolicy)
}
if opts.TitlePrefixMinLength <= 0 {
opts.TitlePrefixMinLength = p.EffectiveConfig().TopData.Wiki.TitlePrefixMinLength
}
namespaces := opts.Namespaces
if len(namespaces) == 0 {
@@ -173,7 +180,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
result.LocalPages = len(pages)
result.Manifest = manifestPath
progress(fmt.Sprintf("NodeBB wiki plan: create %d, update %d, skip %d, stale %d, archive %d, drift %d", result.Created, result.Updated, result.Skipped, result.Stale, result.Archived, result.Drifted))
progress(fmt.Sprintf("NodeBB wiki plan: create %d, update %d, rename %d, skip %d, stale %d, archive %d, drift %d", result.Created, result.Updated, result.Renamed, result.Skipped, result.Stale, result.Archived, result.Drifted))
if opts.DryRun {
return result, nil
}
@@ -215,6 +222,10 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
if err := client.updatePost(plan.Entry.TID, plan.Entry.PID, plan.Content, summary); err != nil {
return result, fmt.Errorf("deploy wiki page %q: archive NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
}
case "rename":
if err := client.renameWikiPage(plan.Entry.TID, plan.Entry.CID, plan.Title); err != nil {
return result, fmt.Errorf("deploy wiki page %q: rename NodeBB topic %d to %q: %w", plan.Page.PageID, plan.Entry.TID, plan.Title, err)
}
}
}
if err := saveDeployManifest(manifestPath, nextManifest); err != nil {
@@ -346,6 +357,7 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
entry.Hash = page.Hash
entry.LastSeenHash = page.Hash
entry.Title = page.Title
entry.TopicTitle = nodeBBTopicTitle(page, opts.TitlePrefixMinLength)
entry.Namespace = page.Namespace
entry.Stale = false
if entry.CID == 0 {
@@ -376,11 +388,22 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
return nil, result, next, fmt.Errorf("wiki page %q requires --category %s=<cid> for creation", pageID, page.Namespace)
}
result.Created++
page.Title = nodeBBTopicTitle(page)
page.Title = entry.TopicTitle
plans = append(plans, wikiDeployPlan{Page: page, Entry: entry, Action: "create", Content: page.Content})
continue
}
if entry.TID != 0 && entry.CID != 0 && shouldCheckRemoteTopicTitle(manifest.Pages[pageID], page.Title, entry.TopicTitle, opts.TitlePrefixMinLength) {
remotePage, ok, err := findMappedRemotePage(entry, remotePagesByCID, client)
if err != nil {
return nil, result, next, err
}
if ok && titleNeedsRename(remotePageTitle(remotePage), entry.TopicTitle) {
result.Renamed++
plans = append(plans, wikiDeployPlan{Page: page, Entry: entry, Action: "rename", Title: entry.TopicTitle})
}
}
if manifest.Pages[pageID].Hash == page.Hash {
result.Skipped++
continue
@@ -442,6 +465,49 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
return plans, result, next, nil
}
func findMappedRemotePage(entry wikiDeployManifestPage, remotePagesByCID map[int][]nodeBBWikiPage, client *nodeBBClient) (nodeBBWikiPage, bool, error) {
remotePages, ok := remotePagesByCID[entry.CID]
if !ok {
var err error
remotePages, err = client.listNamespacePages(entry.CID)
if err != nil {
return nodeBBWikiPage{}, false, err
}
remotePagesByCID[entry.CID] = remotePages
}
for _, remotePage := range remotePages {
if remotePage.TID == entry.TID {
return remotePage, true, nil
}
}
return nodeBBWikiPage{}, false, nil
}
func remotePageTitle(page nodeBBWikiPage) string {
if title := strings.TrimSpace(page.Title); title != "" {
return title
}
return strings.TrimSpace(page.TitleLeaf)
}
func titleNeedsRename(current, desired string) bool {
current = strings.TrimSpace(current)
desired = strings.TrimSpace(desired)
return current != "" && desired != "" && current != desired
}
func shouldCheckRemoteTopicTitle(previous wikiDeployManifestPage, pageTitle, desiredTopicTitle string, minLength int) bool {
if strings.TrimSpace(previous.TopicTitle) != "" {
return strings.TrimSpace(previous.TopicTitle) != strings.TrimSpace(desiredTopicTitle)
}
titleLen := len([]rune(strings.TrimSpace(pageTitle)))
if titleLen == 0 {
return false
}
const legacyPrefixMinLength = 5
return titleLen < legacyPrefixMinLength || titleLen < minLength
}
func recoverCreateCollision(plan wikiDeployPlan, manifest wikiDeployManifest, client *nodeBBClient, summary string) (bool, error) {
entry, ok, err := findExistingNodeBBPage(plan.Page, plan.Entry.CID, client)
if err != nil || !ok {
@@ -461,6 +527,7 @@ func recoverCreateCollision(plan wikiDeployPlan, manifest wikiDeployManifest, cl
entry.Hash = plan.Entry.Hash
entry.LastSeenHash = plan.Entry.LastSeenHash
entry.Title = plan.Entry.Title
entry.TopicTitle = plan.Entry.TopicTitle
entry.Namespace = plan.Entry.Namespace
entry.Stale = false
manifest.Pages[plan.Page.PageID] = entry
@@ -831,9 +898,12 @@ func extractHTMLTitle(content, fallback string) string {
return namespaceTitle(parts[len(parts)-1])
}
func nodeBBTopicTitle(page wikiDeployPage) string {
func nodeBBTopicTitle(page wikiDeployPage, minLength int) string {
if minLength <= 0 {
minLength = project.DefaultTopDataWikiTitlePrefixMinLength
}
title := strings.TrimSpace(page.Title)
if len([]rune(title)) >= 5 {
if len([]rune(title)) >= minLength {
return title
}
namespace := strings.TrimSpace(namespaceTitle(page.Namespace))
@@ -1007,6 +1077,14 @@ func (c *nodeBBClient) updatePost(tid, pid int, content, summary string) error {
return c.request("PUT", fmt.Sprintf("/api/v3/posts/%d", pid), body, nil)
}
func (c *nodeBBClient) renameWikiPage(tid, cid int, title string) error {
if tid == 0 || cid == 0 {
return fmt.Errorf("NodeBB wiki page rename requires topic id and category id")
}
body := map[string]any{"tid": tid, "cid": cid, "title": title}
return c.request("PUT", "/api/v3/plugins/westgate-wiki/page/move", body, nil)
}
func (c *nodeBBClient) acquireEditLock(tid int) (nodeBBEditLock, error) {
if tid == 0 {
return nodeBBEditLock{}, fmt.Errorf("NodeBB wiki post update requires a topic id for edit locking")