mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
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:
6 files changed
+373
-116
No files matched your search
@@ -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
|
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
@@ -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"},
|
||||||
} {
|
} {
|
||||||
|
|||||||
+267
-92
@@ -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{
|
||||||
|
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)
|
target, err := a.resolve(cmd.Context(), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
items, err := (services.Wiki{Client: target.client, Repository: target.Repo}).List(cmd.Context(), limit, page)
|
result, err := (services.Wiki{Client: target.client, Repository: target.Repo}).ListPage(cmd.Context(), limit, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeList(a, cmd, items, func(item services.WikiPage) []terminal.Cell {
|
spec := listSpec[services.WikiPage]{
|
||||||
return terminal.Row(item.Title, item.Author, item.UpdatedAt)
|
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}}
|
||||||
pageFlags(list, &limit, &page)
|
},
|
||||||
group.AddCommand(list)
|
Context: "wiki pages in " + target.Repo, Empty: "no wiki pages in " + target.Repo,
|
||||||
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 {
|
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)
|
target, err := a.resolve(cmd.Context(), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
items, err := (services.Wiki{Client: target.client, Repository: target.Repo}).Revisions(cmd.Context(), args[0], revisionPage)
|
item, err := (services.Wiki{Client: target.client, Repository: target.Repo}).View(cmd.Context(), args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeList(a, cmd, items, func(item services.WikiRevision) []terminal.Cell {
|
if web {
|
||||||
return terminal.Row(item.SHA, item.Author, item.CreatedAt, item.Message)
|
return a.openBrowser(cmd, item.URL)
|
||||||
})
|
|
||||||
}}
|
|
||||||
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" {
|
return writeView(a, cmd, item, a.wikiView)
|
||||||
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().BoolVarP(&web, "web", "w", false, "Open the page in the browser")
|
||||||
command.Flags().StringVar(&file, "body-file", "", "Read content from file, or - for stdin")
|
command.ValidArgsFunction = a.completeWikiPages()
|
||||||
command.Flags().StringVar(&message, "message", "", "Wiki commit message")
|
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]
|
||||||
}
|
}
|
||||||
command.RunE = func(cmd *cobra.Command, args []string) (runErr error) {
|
|
||||||
var editorDraft terminal.Draft
|
|
||||||
defer finishDraft(&editorDraft, &runErr)
|
|
||||||
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"))
|
|
||||||
}
|
}
|
||||||
var err error
|
body, err := a.readBody(cmd, content.body, content.file)
|
||||||
if body, err = a.readBody(cmd, body, file); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if verb == "create" {
|
|
||||||
prompted, err = a.require(cmd, &title, "title")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
if _, err := a.requireInput(cmd, &content.title, "title", "Page title", nil); err != nil {
|
||||||
if !useEditor && !cmd.Flags().Changed("body") && !cmd.Flags().Changed("body-file") {
|
|
||||||
asked, err := a.require(cmd, &body, "body")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
prompted = prompted || asked
|
if strings.TrimSpace(content.title) == "" {
|
||||||
}
|
|
||||||
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"))
|
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
|
||||||
|
}
|
||||||
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
initial = item.Body
|
body = current.Body
|
||||||
}
|
if content.editor {
|
||||||
editorDraft, err = a.editDraft(cmd, initial)
|
if draft, err = a.editDraft(cmd, current.Body); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
body = editorDraft.Body
|
if body = draft.Body; strings.TrimSpace(body) == "" {
|
||||||
if strings.TrimSpace(body) == "" {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("wiki content cannot be empty"))
|
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 {
|
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)
|
||||||
|
} else {
|
||||||
|
a.done(cmd, "Edited wiki page %q in %s", item.Title, target.Repo)
|
||||||
}
|
}
|
||||||
group.AddCommand(command)
|
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
|
||||||
}
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user