feat(wiki): rename pages with --title alone, page lists on the client

wiki edit P --title New failed with "supply --body": renaming now keeps
the page's content, and edit needs one of --title, --body, --body-file
or --editor. wiki list --limit 1 returned every page because Gitea
replaces a limit of 1 with its default; the page list is now read whole
and paged on the client, so --limit and --page hold and the context line
counts the pages.

wiki create takes the title as an argument and, without --body, asks for
the content in the editor or an inline multi-line field. wiki view shows
the author, last update and revision count above the rendered page, and
--web opens it. wiki list and revisions show headed columns, page names
complete with Tab, and every change reports a ✓ line.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:52:33 +02:00
1 parent 78b5ff3a19
commit e093610ade
6 files changed
+400 -143

No files matched your search

-7
View File
@@ -494,12 +494,6 @@ const (
time delete time delete
time list time list
time reset time reset
wiki create
wiki delete
wiki edit
wiki list
wiki revisions
wiki view
` `
longsToWrite = ` longsToWrite = `
issue issue
@@ -512,6 +506,5 @@ const (
pr attachment pr attachment
pr comment pr comment
time time
wiki
` `
) )
+1 -2
View File
@@ -240,7 +240,7 @@ func TestUsageErrorsPrecedeRequests(t *testing.T) {
{[]string{"webhook", "delete", "not-an-id"}, "invalid resource number \"not-an-id\""}, {[]string{"webhook", "delete", "not-an-id"}, "invalid resource number \"not-an-id\""},
{[]string{"webhook", "list", "--org", "organization", "--account"}, "--org, --account, --global, and --repo select mutually exclusive scopes"}, {[]string{"webhook", "list", "--org", "organization", "--account"}, "--org, --account, --global, and --repo select mutually exclusive scopes"},
{[]string{"webhook", "list", "--global", "-R", "owner/repo"}, "--org, --account, --global, and --repo select mutually exclusive scopes"}, {[]string{"webhook", "list", "--global", "-R", "owner/repo"}, "--org, --account, --global, and --repo select mutually exclusive scopes"},
{[]string{"wiki", "create", "--body-file", "-"}, "--body-file - requires --title"}, {[]string{"wiki", "create", "--body-file", "-"}, "--body-file - requires a title"},
{[]string{"wiki", "create", "--title", "Home", "--body", "text", "--json=created_at"}, "unknown JSON field \"created_at\""}, {[]string{"wiki", "create", "--title", "Home", "--body", "text", "--json=created_at"}, "unknown JSON field \"created_at\""},
{[]string{"wiki", "edit", "Home", "--body="}, "wiki content cannot be empty"}, {[]string{"wiki", "edit", "Home", "--body="}, "wiki content cannot be empty"},
{[]string{"wiki", "revisions", "Home", "--page", "0"}, `"--page" flag: must be a positive integer`}, {[]string{"wiki", "revisions", "Home", "--page", "0"}, `"--page" flag: must be a positive integer`},
@@ -258,7 +258,6 @@ func TestConfirmationsNameResolvedValues(t *testing.T) {
inputs map[string]string inputs map[string]string
want string want string
}{ }{
{[]string{"wiki", "create"}, map[string]string{"title": "Home", "body": "text"}, "create wiki page Home on alice/project"},
{[]string{"release", "create"}, map[string]string{"Tag": "v1.0.0"}, "Create release v1.0.0 in alice/project?"}, {[]string{"release", "create"}, map[string]string{"Tag": "v1.0.0"}, "Create release v1.0.0 in alice/project?"},
{[]string{"time", "delete", "5", "7"}, nil, "delete tracked time entry 7 on alice/project #5"}, {[]string{"time", "delete", "5", "7"}, nil, "delete tracked time entry 7 on alice/project #5"},
} { } {
+294 -119
View File
@@ -4,8 +4,10 @@
package app package app
import ( import (
"context"
"errors" "errors"
"math" "math"
"strconv"
"strings" "strings"
"gitea.dev/tea/internal/clierr" "gitea.dev/tea/internal/clierr"
@@ -14,146 +16,319 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func wikiRecord(item services.WikiPage) terminal.Record { func (a *application) wikiCommand() *cobra.Command {
return terminal.Record{Heading: []terminal.Cell{{Text: item.Title, Bold: true}}, Lines: []string{item.Body, item.URL}} group := &cobra.Command{
Use: "wiki",
Short: "Manage repository wiki pages",
Long: "List, view, create, edit and delete the pages of a repository's wiki, and list their revisions.",
}
group.AddCommand(a.wikiListCommand(), a.wikiViewCommand(), a.wikiRevisionsCommand(), a.wikiCreateCommand(), a.wikiEditCommand(), a.wikiDeleteCommand())
return group
} }
func (a *application) wikiCommand() *cobra.Command { // completeWikiPages completes the titles of the repository's wiki pages.
group := &cobra.Command{Use: "wiki", Short: "Manage repository wiki pages"} func (a *application) completeWikiPages() cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
pages, err := services.Wiki{Client: t.client, Repository: t.Repo}.List(ctx, completionLimit, 1)
return describe(pages, func(page services.WikiPage) (string, string) { return page.Title, "" }), err
})
}
func (a *application) wikiListCommand() *cobra.Command {
var limit, page int var limit, page int
list := &cobra.Command{Use: "list", Short: "List wiki pages", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error { command := &cobra.Command{
target, err := a.resolve(cmd.Context(), true) Use: "list",
if err != nil { Short: "List wiki pages",
return err Example: "$ tea wiki list\n$ tea wiki list -R alice/project --json title,url",
} Args: argsExact(0),
items, err := (services.Wiki{Client: target.client, Repository: target.Repo}).List(cmd.Context(), limit, page) RunE: func(cmd *cobra.Command, _ []string) error {
if err != nil { target, err := a.resolve(cmd.Context(), true)
return err if err != nil {
} return err
return writeList(a, cmd, items, func(item services.WikiPage) []terminal.Cell { }
return terminal.Row(item.Title, item.Author, item.UpdatedAt) result, err := (services.Wiki{Client: target.client, Repository: target.Repo}).ListPage(cmd.Context(), limit, page)
}) if err != nil {
}} return err
pageFlags(list, &limit, &page) }
group.AddCommand(list) spec := listSpec[services.WikiPage]{
var revisionPage int Columns: []terminal.Column{{Header: "Title", Flex: true}, {Header: "Author"}, {Header: "Updated"}, {Header: "URL", PipedOnly: true}},
revisions := &cobra.Command{Use: "revisions <page>", Short: "List revisions of a wiki page", Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error { Row: func(item services.WikiPage) []terminal.Cell {
target, err := a.resolve(cmd.Context(), true) return []terminal.Cell{{Text: item.Title, Link: item.URL}, {Text: item.Author}, {Time: terminal.ParseTime(item.UpdatedAt)}, {Text: item.URL}}
if err != nil { },
return err Context: "wiki pages in " + target.Repo, Empty: "no wiki pages in " + target.Repo,
} }
items, err := (services.Wiki{Client: target.client, Repository: target.Repo}).Revisions(cmd.Context(), args[0], revisionPage) return writeTable(a, cmd, result.Items, spec, listPage{Number: page, More: result.More, Total: result.Total, Counted: result.Counted})
if err != nil { },
return err }
} pageFlags(command, &limit, &page)
return writeList(a, cmd, items, func(item services.WikiRevision) []terminal.Cell { return command
return terminal.Row(item.SHA, item.Author, item.CreatedAt, item.Message) }
})
}} func (a *application) wikiViewCommand() *cobra.Command {
positiveIntVar(revisions, &revisionPage, "page", 1, math.MaxInt, "Revision page (server controls page size)") var web bool
group.AddCommand(revisions) command := &cobra.Command{
for _, verb := range []string{"view", "create", "edit", "delete"} { Use: "view <page>",
var title, body, file, message string Short: "View a wiki page",
var useEditor bool Long: "View a wiki page, rendered on a terminal.",
command := &cobra.Command{Use: verb + " <page>", Short: titleCase(verb) + " a wiki page", Args: argsExact(1)} Example: "$ tea wiki view Home\n$ tea wiki view \"Getting Started\" --web",
switch verb { Args: argsExact(1),
case "create": RunE: func(cmd *cobra.Command, args []string) error {
command.Use = verb target, err := a.resolve(cmd.Context(), true)
command.Args = argsExact(0) if err != nil {
case "delete": return err
command.Annotations = textOutput("wiki page deletion does not return a resource") }
} item, err := (services.Wiki{Client: target.client, Repository: target.Repo}).View(cmd.Context(), args[0])
if verb == "create" || verb == "edit" { if err != nil {
command.Flags().StringVar(&title, "title", "", "Page title") return err
command.Flags().BoolVar(&useEditor, "editor", false, "Edit page content in an editor") }
command.Flags().StringVar(&body, "body", "", "Page content") if web {
command.Flags().StringVar(&file, "body-file", "", "Read content from file, or - for stdin") return a.openBrowser(cmd, item.URL)
command.Flags().StringVar(&message, "message", "", "Wiki commit message") }
} return writeView(a, cmd, item, a.wikiView)
command.RunE = func(cmd *cobra.Command, args []string) (runErr error) { },
var editorDraft terminal.Draft }
defer finishDraft(&editorDraft, &runErr) command.Flags().BoolVarP(&web, "web", "w", false, "Open the page in the browser")
command.ValidArgsFunction = a.completeWikiPages()
return command
}
// wikiView shows a wiki page with its content as the body.
func (a *application) wikiView(item services.WikiPage) terminal.View {
var meta []string
if item.Author != "" {
meta = append(meta, item.Author)
}
if updated := terminal.ParseTime(item.UpdatedAt); !updated.IsZero() {
meta = append(meta, "updated "+terminal.RelativeTime(a.deps.Now(), updated))
}
if item.CommitCount > 0 {
meta = append(meta, countNoun(int(item.CommitCount), "revision"))
}
return terminal.View{
Title: item.Title, Meta: strings.Join(meta, " · "), Body: item.Body, Kind: "wiki page", URL: item.URL,
Fields: []terminal.Field{{Label: "Author", Value: item.Author, PipedOnly: true}, {Label: "Updated", Value: item.UpdatedAt, PipedOnly: true}},
}
}
func (a *application) wikiRevisionsCommand() *cobra.Command {
var page int
command := &cobra.Command{
Use: "revisions <page>",
Short: "List revisions of a wiki page",
Example: "$ tea wiki revisions Home",
Args: argsExact(1),
RunE: func(cmd *cobra.Command, args []string) error {
target, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
items, err := (services.Wiki{Client: target.client, Repository: target.Repo}).Revisions(cmd.Context(), args[0], page)
if err != nil {
return err
}
spec := listSpec[services.WikiRevision]{
Columns: []terminal.Column{{Header: "Commit"}, {Header: "Author"}, {Header: "Date"}, {Header: "Message", Flex: true}},
Row: func(item services.WikiRevision) []terminal.Cell {
message, _, _ := strings.Cut(item.Message, "\n")
sha := item.SHA
if a.deps.Streams.OutTTY {
sha = shortSHA(sha)
}
return []terminal.Cell{{Text: sha, Muted: true}, {Text: item.Author}, {Time: terminal.ParseTime(item.CreatedAt)}, {Text: message}}
},
Context: "revisions of wiki page " + strconv.Quote(args[0]), Empty: "no revisions of wiki page " + strconv.Quote(args[0]),
}
return writeTable(a, cmd, items, spec, listPage{Number: page, Counted: false, More: false})
},
}
positiveIntVar(command, &page, "page", 1, math.MaxInt, "Revision page (server controls page size)")
command.ValidArgsFunction = a.completeWikiPages()
return command
}
// wikiContent are the flags of wiki create and wiki edit.
type wikiContent struct {
title, body, file, message string
editor bool
}
func (c *wikiContent) register(cmd *cobra.Command) {
flags := cmd.Flags()
flags.StringVarP(&c.title, "title", "t", "", "Page title")
flags.BoolVarP(&c.editor, "editor", "e", false, "Write the page content in an editor")
flags.StringVarP(&c.body, "body", "b", "", "Page content")
flags.StringVarP(&c.file, "body-file", "F", "", "Read the page content from a file, or - for standard input")
flags.StringVarP(&c.message, "message", "m", "", "Commit message of the change")
}
func (a *application) wikiCreateCommand() *cobra.Command {
var content wikiContent
command := &cobra.Command{
Use: "create [<title>]",
Short: "Create a wiki page",
Long: "Create a wiki page. Without --body or --body-file, the content is written in an editor or typed on the terminal.",
Example: `$ tea wiki create "Getting Started" --body-file docs/start.md
$ tea wiki create Home --editor`,
Args: usageArgs(cobra.MaximumNArgs(1)),
RunE: func(cmd *cobra.Command, args []string) (runErr error) {
var draft terminal.Draft
defer finishDraft(&draft, &runErr)
if len(args) > 0 {
if cmd.Flags().Changed("title") {
return clierr.Wrap(clierr.Usage, errors.New("supply the title as an argument or --title, not both"))
}
content.title = args[0]
}
if err := validateBodySources(cmd); err != nil { if err := validateBodySources(cmd); err != nil {
return err return err
} }
prompted := false if content.file == "-" && content.title == "" {
if verb == "create" || verb == "edit" { return clierr.Wrap(clierr.Usage, errors.New("--body-file - requires a title; stdin cannot also supply a prompt"))
if file == "-" && verb == "create" && title == "" { }
return clierr.Wrap(clierr.Usage, errors.New("--body-file - requires --title; stdin cannot also supply a prompt")) body, err := a.readBody(cmd, content.body, content.file)
} if err != nil {
var err error return err
if body, err = a.readBody(cmd, body, file); err != nil { }
if _, err := a.requireInput(cmd, &content.title, "title", "Page title", nil); err != nil {
return err
}
if strings.TrimSpace(content.title) == "" {
return clierr.Wrap(clierr.Usage, errors.New("wiki title cannot be empty"))
}
target, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
if !cmd.Flags().Changed("body") && !cmd.Flags().Changed("body-file") {
if draft, err = a.wikiBody(cmd, content.editor, ""); err != nil {
return err return err
} }
if verb == "create" { body = draft.Body
prompted, err = a.require(cmd, &title, "title") }
if err != nil { if strings.TrimSpace(body) == "" {
return err return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty"))
} }
} item, err := (services.Wiki{Client: target.client, Repository: target.Repo}).Create(cmd.Context(), services.WikiOptions{Title: content.title, Body: body, Message: content.message})
if !useEditor && !cmd.Flags().Changed("body") && !cmd.Flags().Changed("body-file") { if err != nil {
asked, err := a.require(cmd, &body, "body") return err
if err != nil { }
return err a.done(cmd, "Created wiki page %q in %s", item.Title, target.Repo)
} if a.options.json != "" {
prompted = prompted || asked return a.writeJSON(cmd, item)
} }
if !useEditor && strings.TrimSpace(body) == "" { return a.printURL(cmd, item.URL)
return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty")) },
} }
if (verb == "create" || cmd.Flags().Changed("title")) && strings.TrimSpace(title) == "" { content.register(command)
return clierr.Wrap(clierr.Usage, errors.New("wiki title cannot be empty")) return command
} }
// wikiBody collects page content: in the editor with --editor, else in the editor or an inline
// field on a terminal. Without a terminal it fails, naming --body.
func (a *application) wikiBody(cmd *cobra.Command, editor bool, initial string) (terminal.Draft, error) {
if editor {
return a.editDraft(cmd, initial)
}
s, err := a.deps.Settings.Load()
if err != nil {
return terminal.Draft{}, err
}
if !a.policy(s).CanPrompt(a.deps.Streams) {
return terminal.Draft{}, clierr.Wrap(clierr.Usage, errors.New("missing required input: supply --body, --body-file or --editor"))
}
return a.bodyEditor(cmd)(cmd.Context(), initial)
}
func (a *application) wikiEditCommand() *cobra.Command {
var content wikiContent
command := &cobra.Command{
Use: "edit <page>",
Short: "Edit a wiki page",
Long: "Change the title or the content of a wiki page. Renaming with --title alone keeps the content.",
Example: `$ tea wiki edit Home --body-file docs/home.md --message "Rewrite the home page"
$ tea wiki edit "Old Title" --title "New Title"
$ tea wiki edit Home --editor`,
Args: argsExact(1),
RunE: func(cmd *cobra.Command, args []string) (runErr error) {
var draft terminal.Draft
defer finishDraft(&draft, &runErr)
if err := validateBodySources(cmd); err != nil {
return err
}
changed := cmd.Flags().Changed
if !changed("title") && !changed("body") && !changed("body-file") && !content.editor {
return clierr.Wrap(clierr.Usage, errors.New("supply --title, --body, --body-file or --editor"))
}
if changed("title") && strings.TrimSpace(content.title) == "" {
return clierr.Wrap(clierr.Usage, errors.New("wiki title cannot be empty"))
}
body, err := a.readBody(cmd, content.body, content.file)
if err != nil {
return err
}
if (changed("body") || changed("body-file")) && strings.TrimSpace(body) == "" {
return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty"))
} }
target, err := a.resolve(cmd.Context(), true) target, err := a.resolve(cmd.Context(), true)
if err != nil { if err != nil {
return err return err
} }
service := services.Wiki{Client: target.client, Repository: target.Repo} service := services.Wiki{Client: target.client, Repository: target.Repo}
if useEditor { if content.editor || (!changed("body") && !changed("body-file")) {
initial := "" current, err := service.View(cmd.Context(), args[0])
if verb == "edit" {
item, err := service.View(cmd.Context(), args[0])
if err != nil {
return err
}
initial = item.Body
}
editorDraft, err = a.editDraft(cmd, initial)
if err != nil { if err != nil {
return err return err
} }
body = editorDraft.Body body = current.Body
if strings.TrimSpace(body) == "" { if content.editor {
return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty")) if draft, err = a.editDraft(cmd, current.Body); err != nil {
return err
}
if body = draft.Body; strings.TrimSpace(body) == "" {
return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty"))
}
} }
} }
if verb == "delete" || prompted { item, err := service.Edit(cmd.Context(), args[0], services.WikiOptions{Title: content.title, Body: body, Message: content.message})
subject := title
if verb != "create" {
subject = args[0]
}
if err := a.confirm(cmd, verb+" wiki page "+subject+" on "+target.Repo); err != nil {
return err
}
}
var item services.WikiPage
switch verb {
case "view":
item, err = service.View(cmd.Context(), args[0])
case "create":
item, err = service.Create(cmd.Context(), services.WikiOptions{Title: title, Body: body, Message: message})
case "edit":
item, err = service.Edit(cmd.Context(), args[0], services.WikiOptions{Title: title, Body: body, Message: message})
case "delete":
return service.Delete(cmd.Context(), args[0])
}
if err != nil { if err != nil {
return err return err
} }
return writeRecord(a, cmd, item, wikiRecord) if item.Title != args[0] {
} a.done(cmd, "Renamed wiki page %q to %q in %s", args[0], item.Title, target.Repo)
group.AddCommand(command) } else {
a.done(cmd, "Edited wiki page %q in %s", item.Title, target.Repo)
}
return a.writeMutationJSON(cmd, item)
},
} }
return group content.register(command)
command.ValidArgsFunction = a.completeWikiPages()
return command
}
func (a *application) wikiDeleteCommand() *cobra.Command {
command := &cobra.Command{
Use: "delete <page>",
Short: "Delete a wiki page",
Example: "$ tea wiki delete \"Old Notes\" --yes",
Annotations: map[string]string{"tea.confirm": "true", "tea.text": "wiki page deletion does not return a resource"},
Args: argsExact(1),
RunE: func(cmd *cobra.Command, args []string) error {
target, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
subject := "wiki page " + strconv.Quote(args[0]) + " from " + target.Repo
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Delete " + subject + "?", NonTTY: "delete " + subject}); err != nil {
return err
}
if err := (services.Wiki{Client: target.client, Repository: target.Repo}).Delete(cmd.Context(), args[0]); err != nil {
return err
}
a.done(cmd, "Deleted %s", subject)
return nil
},
}
command.ValidArgsFunction = a.completeWikiPages()
return command
} }
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
func TestWikiEditRenamesAndKeepsTheContent(t *testing.T) {
var requests []string
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests = append(requests, r.Method+" "+r.URL.Path)
if r.Method == http.MethodPatch {
requirePayload(t, `{"title":"New Title","content_base64":"S2VlcCBtZQ=="}`, r.Body)
_, _ = w.Write([]byte(`{"title":"New Title","content_base64":"S2VlcCBtZQ=="}`))
return
}
_, _ = w.Write([]byte(`{"title":"Old Title","content_base64":"S2VlcCBtZQ=="}`))
}))
defer server.Close()
deps, out, diagnostics := repoTTYDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"wiki", "edit", "Old Title", "--title", "New Title", "-R", "alice/x", "--no-input"}))
require.Equal(t, []string{"GET /api/v1/repos/alice/x/wiki/page/Old Title", "PATCH /api/v1/repos/alice/x/wiki/page/Old Title"}, requests)
require.Empty(t, out.String())
require.Equal(t, "✓ Renamed wiki page \"Old Title\" to \"New Title\" in alice/x\n", diagnostics.String())
err := Execute(context.Background(), deps, []string{"wiki", "edit", "Home", "-R", "alice/x"})
require.ErrorContains(t, err, "supply --title, --body, --body-file or --editor")
}
func TestWikiListLimitsPagesOnTheClient(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("page") != "1" {
_, _ = w.Write([]byte(`[]`))
return
}
_, _ = w.Write([]byte(`[{"title":"Getting Started","last_commit":{"author":{"name":"alice","date":"2026-10-11T18:00:00Z"}}},{"title":"Home"}]`))
}))
defer server.Close()
deps, out, _ := repoTTYDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"wiki", "list", "--limit", "1", "-R", "alice/x"}))
require.Equal(t, `Showing 1 of 2 wiki pages in alice/x · next: --page 2
TITLE AUTHOR UPDATED
Getting Started alice about 2 hours ago
`, out.String())
}
func TestWikiViewAndCreate(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
requirePayload(t, `{"title":"Page Two","content_base64":"Ym9keQ=="}`, r.Body)
}
_, _ = w.Write([]byte(`{"title":"Page Two","html_url":"https://gitea.test/alice/x/wiki/Page-Two","content_base64":"Ym9keQ==","commit_count":3,
"last_commit":{"author":{"name":"alice","date":"2026-10-11T19:00:00Z"}}}`))
}))
defer server.Close()
deps, out, diagnostics := repoTTYDeps(t, server.URL)
deps.Getenv = serverEnv(server.URL, map[string]string{"TERM": "dumb", "TEA_MARKDOWN": "raw"})
require.NoError(t, Execute(context.Background(), deps, []string{"wiki", "view", "Page Two", "-R", "alice/x"}))
require.Equal(t, "Page Two\nalice · updated about 1 hour ago · 3 revisions\n\nbody\n\nView this wiki page on Gitea: https://gitea.test/alice/x/wiki/Page-Two\n", out.String())
out.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"wiki", "create", "Page Two", "-b", "body", "-R", "alice/x"}))
require.Equal(t, "https://gitea.test/alice/x/wiki/Page-Two\n", out.String())
require.Equal(t, "✓ Created wiki page \"Page Two\" in alice/x\n", diagnostics.String())
}
+22 -8
View File
@@ -82,27 +82,41 @@ func (s Wiki) page(ctx context.Context, op repoOperation[*gitea.WikiPage]) (Wiki
// List returns a non-nil page of wiki metadata. // List returns a non-nil page of wiki metadata.
func (s Wiki) List(ctx context.Context, limit, page int) ([]WikiPage, error) { func (s Wiki) List(ctx context.Context, limit, page int) ([]WikiPage, error) {
pages, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) ([]*gitea.WikiPageMetaData, *gitea.Response, error) { window, err := s.ListPage(ctx, limit, page)
return c.Wiki.ListPages(ctx, owner, repo, gitea.ListWikiPagesOptions{ListOptions: listOptions(limit, page)}) return window.Items, err
}
// wikiPageSize is the page size for reading the whole wiki; Gitea replaces a limit of 1 with its
// default page size.
const wikiPageSize = 50
// ListPage is List that also reports whether more pages follow and how many there are. It reads
// the whole page list and pages it on the client, because the server pages it inconsistently.
func (s Wiki) ListPage(ctx context.Context, limit, page int) (Page[WikiPage], error) {
all, err := listAll(func(page int) ([]*gitea.WikiPageMetaData, error) {
return repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) ([]*gitea.WikiPageMetaData, *gitea.Response, error) {
return c.Wiki.ListPages(ctx, owner, repo, gitea.ListWikiPagesOptions{ListOptions: listOptions(wikiPageSize, page)})
})
}) })
if err != nil { if err != nil {
if apiError, ok := errors.AsType[*transport.APIError](err); !ok || apiError.Status != http.StatusNotFound { if apiError, ok := errors.AsType[*transport.APIError](err); !ok || apiError.Status != http.StatusNotFound {
return nil, err return Page[WikiPage]{}, err
} }
// An uninitialized wiki returns 404. Verify repository visibility and // An uninitialized wiki returns 404. Verify repository visibility and
// wiki configuration before treating that response as an empty list. // wiki configuration before treating that response as an empty list.
repository, checkErr := (Repositories{Client: s.Client}).get(ctx, s.Repository) repository, checkErr := (Repositories{Client: s.Client}).get(ctx, s.Repository)
if checkErr != nil { if checkErr != nil {
return nil, checkErr return Page[WikiPage]{}, checkErr
} }
if !repository.HasWiki { if !repository.HasWiki {
return nil, err return Page[WikiPage]{}, err
} }
return []WikiPage{}, nil return Page[WikiPage]{Items: []WikiPage{}, Counted: true}, nil
} }
return convertAll(pages, func(item *gitea.WikiPageMetaData) WikiPage { items, err := pageOf(convertAll(all, func(item *gitea.WikiPageMetaData) WikiPage {
return wikiPageFrom(item.Title, item.HTMLURL, item.LastCommit) return wikiPageFrom(item.Title, item.HTMLURL, item.LastCommit)
}), nil }), limit, page)
return Page[WikiPage]{Items: items, More: page*limit < len(all), Total: len(all), Counted: true}, err
} }
// View retrieves and decodes a wiki page. // View retrieves and decodes a wiki page.
+13 -7
View File
@@ -75,9 +75,12 @@ func TestWikiListAndRevisionsMapCommits(t *testing.T) {
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case "/api/v1/repos/alice/project/wiki/pages": case "/api/v1/repos/alice/project/wiki/pages":
assert.Equal(t, "5", r.URL.Query().Get("limit")) assert.Equal(t, "50", r.URL.Query().Get("limit"))
assert.Equal(t, "2", r.URL.Query().Get("page")) if r.URL.Query().Get("page") != "1" {
_, _ = w.Write([]byte(`[{"title":"Home","html_url":"https://gitea.test/wiki/Home","last_commit":{"sha":"abc","author":{"name":"Alice","email":"author@example.test","date":"2026-10-04T00:00:00Z"}}},{"title":"Draft"}]`)) _, _ = w.Write([]byte(`[]`))
return
}
_, _ = w.Write([]byte(`[{"title":"Other"},{"title":"Home","html_url":"https://gitea.test/wiki/Home","last_commit":{"sha":"abc","author":{"name":"Alice","email":"author@example.test","date":"2026-10-04T00:00:00Z"}}},{"title":"Draft"}]`))
case "/api/v1/repos/alice/project/wiki/revisions/Home": case "/api/v1/repos/alice/project/wiki/revisions/Home":
assert.Equal(t, "3", r.URL.Query().Get("page")) assert.Equal(t, "3", r.URL.Query().Get("page"))
_, _ = w.Write([]byte(`{"commits":[{"sha":"abc","message":"Edit","author":{"name":"Alice","email":"author@example.test","date":"2026-10-04T00:00:00Z"}},{"sha":"def","author":null}],"count":2}`)) _, _ = w.Write([]byte(`{"commits":[{"sha":"abc","message":"Edit","author":{"name":"Alice","email":"author@example.test","date":"2026-10-04T00:00:00Z"}},{"sha":"def","author":null}],"count":2}`))
@@ -86,12 +89,15 @@ func TestWikiListAndRevisionsMapCommits(t *testing.T) {
} }
})) }))
service := Wiki{Client: client, Repository: "alice/project"} service := Wiki{Client: client, Repository: "alice/project"}
pages, err := service.List(context.Background(), 5, 2) pages, err := service.ListPage(context.Background(), 2, 1)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []WikiPage{ require.Equal(t, Page[WikiPage]{Items: []WikiPage{
{Title: "Other"},
{Title: "Home", URL: "https://gitea.test/wiki/Home", SHA: "abc", Author: "Alice", UpdatedAt: "2026-10-04T00:00:00Z"}, {Title: "Home", URL: "https://gitea.test/wiki/Home", SHA: "abc", Author: "Alice", UpdatedAt: "2026-10-04T00:00:00Z"},
{Title: "Draft"}, }, More: true, Total: 3, Counted: true}, pages)
}, pages) last, err := service.List(context.Background(), 2, 2)
require.NoError(t, err)
require.Equal(t, []WikiPage{{Title: "Draft"}}, last)
revisions, err := service.Revisions(context.Background(), "Home", 3) revisions, err := service.Revisions(context.Background(), "Home", 3)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []WikiRevision{{SHA: "abc", Message: "Edit", Author: "Alice", CreatedAt: "2026-10-04T00:00:00Z"}, {SHA: "def"}}, revisions) require.Equal(t, []WikiRevision{{SHA: "abc", Message: "Edit", Author: "Alice", CreatedAt: "2026-10-04T00:00:00Z"}, {SHA: "def"}}, revisions)