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 list
time reset
wiki create
wiki delete
wiki edit
wiki list
wiki revisions
wiki view
`
longsToWrite = `
issue
@@ -512,6 +506,5 @@ const (
pr attachment
pr comment
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", "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{"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", "edit", "Home", "--body="}, "wiki content cannot be empty"},
{[]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
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{"time", "delete", "5", "7"}, nil, "delete tracked time entry 7 on alice/project #5"},
} {
+294 -119
View File
@@ -4,8 +4,10 @@
package app
import (
"context"
"errors"
"math"
"strconv"
"strings"
"gitea.dev/tea/internal/clierr"
@@ -14,146 +16,319 @@ import (
"github.com/spf13/cobra"
)
func wikiRecord(item services.WikiPage) terminal.Record {
return terminal.Record{Heading: []terminal.Cell{{Text: item.Title, Bold: true}}, Lines: []string{item.Body, item.URL}}
func (a *application) wikiCommand() *cobra.Command {
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 {
group := &cobra.Command{Use: "wiki", Short: "Manage repository wiki pages"}
// completeWikiPages completes the titles of the repository's 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
list := &cobra.Command{Use: "list", Short: "List wiki pages", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
target, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
items, err := (services.Wiki{Client: target.client, Repository: target.Repo}).List(cmd.Context(), limit, page)
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)
})
}}
pageFlags(list, &limit, &page)
group.AddCommand(list)
var revisionPage int
revisions := &cobra.Command{Use: "revisions <page>", Short: "List revisions of a wiki page", 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], revisionPage)
if err != nil {
return err
}
return writeList(a, cmd, items, func(item services.WikiRevision) []terminal.Cell {
return terminal.Row(item.SHA, item.Author, item.CreatedAt, item.Message)
})
}}
positiveIntVar(revisions, &revisionPage, "page", 1, math.MaxInt, "Revision page (server controls page size)")
group.AddCommand(revisions)
for _, verb := range []string{"view", "create", "edit", "delete"} {
var title, body, file, message string
var useEditor bool
command := &cobra.Command{Use: verb + " <page>", Short: titleCase(verb) + " a wiki page", Args: argsExact(1)}
switch verb {
case "create":
command.Use = verb
command.Args = argsExact(0)
case "delete":
command.Annotations = textOutput("wiki page deletion does not return a resource")
}
if verb == "create" || verb == "edit" {
command.Flags().StringVar(&title, "title", "", "Page title")
command.Flags().BoolVar(&useEditor, "editor", false, "Edit page content in an editor")
command.Flags().StringVar(&body, "body", "", "Page content")
command.Flags().StringVar(&file, "body-file", "", "Read content from file, or - for stdin")
command.Flags().StringVar(&message, "message", "", "Wiki commit message")
}
command.RunE = func(cmd *cobra.Command, args []string) (runErr error) {
var editorDraft terminal.Draft
defer finishDraft(&editorDraft, &runErr)
command := &cobra.Command{
Use: "list",
Short: "List wiki pages",
Example: "$ tea wiki list\n$ tea wiki list -R alice/project --json title,url",
Args: argsExact(0),
RunE: func(cmd *cobra.Command, _ []string) error {
target, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
result, err := (services.Wiki{Client: target.client, Repository: target.Repo}).ListPage(cmd.Context(), limit, page)
if err != nil {
return err
}
spec := listSpec[services.WikiPage]{
Columns: []terminal.Column{{Header: "Title", Flex: true}, {Header: "Author"}, {Header: "Updated"}, {Header: "URL", PipedOnly: true}},
Row: func(item services.WikiPage) []terminal.Cell {
return []terminal.Cell{{Text: item.Title, Link: item.URL}, {Text: item.Author}, {Time: terminal.ParseTime(item.UpdatedAt)}, {Text: item.URL}}
},
Context: "wiki pages in " + target.Repo, Empty: "no wiki pages in " + target.Repo,
}
return writeTable(a, cmd, result.Items, spec, listPage{Number: page, More: result.More, Total: result.Total, Counted: result.Counted})
},
}
pageFlags(command, &limit, &page)
return command
}
func (a *application) wikiViewCommand() *cobra.Command {
var web bool
command := &cobra.Command{
Use: "view <page>",
Short: "View a wiki page",
Long: "View a wiki page, rendered on a terminal.",
Example: "$ tea wiki view Home\n$ tea wiki view \"Getting Started\" --web",
Args: argsExact(1),
RunE: func(cmd *cobra.Command, args []string) error {
target, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
item, err := (services.Wiki{Client: target.client, Repository: target.Repo}).View(cmd.Context(), args[0])
if err != nil {
return err
}
if web {
return a.openBrowser(cmd, item.URL)
}
return writeView(a, cmd, item, a.wikiView)
},
}
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 {
return err
}
prompted := false
if verb == "create" || verb == "edit" {
if file == "-" && verb == "create" && title == "" {
return clierr.Wrap(clierr.Usage, errors.New("--body-file - requires --title; stdin cannot also supply a prompt"))
}
var err error
if body, err = a.readBody(cmd, body, file); err != nil {
if content.file == "-" && content.title == "" {
return clierr.Wrap(clierr.Usage, errors.New("--body-file - requires a title; stdin cannot also supply a prompt"))
}
body, err := a.readBody(cmd, content.body, content.file)
if err != nil {
return err
}
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
}
if verb == "create" {
prompted, err = a.require(cmd, &title, "title")
if err != nil {
return err
}
}
if !useEditor && !cmd.Flags().Changed("body") && !cmd.Flags().Changed("body-file") {
asked, err := a.require(cmd, &body, "body")
if err != nil {
return err
}
prompted = prompted || asked
}
if !useEditor && strings.TrimSpace(body) == "" {
return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty"))
}
if (verb == "create" || cmd.Flags().Changed("title")) && strings.TrimSpace(title) == "" {
return clierr.Wrap(clierr.Usage, errors.New("wiki title cannot be empty"))
}
body = draft.Body
}
if strings.TrimSpace(body) == "" {
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 err != nil {
return err
}
a.done(cmd, "Created wiki page %q in %s", item.Title, target.Repo)
if a.options.json != "" {
return a.writeJSON(cmd, item)
}
return a.printURL(cmd, item.URL)
},
}
content.register(command)
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)
if err != nil {
return err
}
service := services.Wiki{Client: target.client, Repository: target.Repo}
if useEditor {
initial := ""
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 content.editor || (!changed("body") && !changed("body-file")) {
current, err := service.View(cmd.Context(), args[0])
if err != nil {
return err
}
body = editorDraft.Body
if strings.TrimSpace(body) == "" {
return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty"))
body = current.Body
if content.editor {
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 {
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])
}
item, err := service.Edit(cmd.Context(), args[0], services.WikiOptions{Title: content.title, Body: body, Message: content.message})
if err != nil {
return err
}
return writeRecord(a, cmd, item, wikiRecord)
}
group.AddCommand(command)
if item.Title != args[0] {
a.done(cmd, "Renamed wiki page %q to %q in %s", args[0], item.Title, target.Repo)
} 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.
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) {
return c.Wiki.ListPages(ctx, owner, repo, gitea.ListWikiPagesOptions{ListOptions: listOptions(limit, page)})
window, err := s.ListPage(ctx, 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 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
// wiki configuration before treating that response as an empty list.
repository, checkErr := (Repositories{Client: s.Client}).get(ctx, s.Repository)
if checkErr != nil {
return nil, checkErr
return Page[WikiPage]{}, checkErr
}
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)
}), 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.
+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) {
switch r.URL.Path {
case "/api/v1/repos/alice/project/wiki/pages":
assert.Equal(t, "5", r.URL.Query().Get("limit"))
assert.Equal(t, "2", r.URL.Query().Get("page"))
_, _ = 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"}]`))
assert.Equal(t, "50", r.URL.Query().Get("limit"))
if r.URL.Query().Get("page") != "1" {
_, _ = 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":
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}`))
@@ -86,12 +89,15 @@ func TestWikiListAndRevisionsMapCommits(t *testing.T) {
}
}))
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.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: "Draft"},
}, pages)
}, More: true, Total: 3, Counted: true}, 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)
require.NoError(t, err)
require.Equal(t, []WikiRevision{{SHA: "abc", Message: "Edit", Author: "Alice", CreatedAt: "2026-10-04T00:00:00Z"}, {SHA: "def"}}, revisions)