mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
The application caches the settings snapshot and reloads it only after an update through the store, so output and prompt helpers no longer read and parse config.yml and hosts.yml several times per command. canPrompt and chooser replace the inline prompt-policy checks. Checks are tallied by one services.Tally, terminal.Plural replaces the two app copies, refKind.article/noun replace the comment group's own words, list commands use servicePage, and web URLs are built by repositoryURL and webURL in browser.go. Signed-off-by: bircni <bircni@icloud.com>
198 lines
8.6 KiB
Go
198 lines
8.6 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.dev/tea/internal/clierr"
|
|
"gitea.dev/tea/internal/services"
|
|
"gitea.dev/tea/internal/terminal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// attachmentName names an attachment in messages: "attachment 9 (log.txt) on alice/x#1".
|
|
func attachmentName(t target, n int64, item services.IssueAttachment) string {
|
|
return fmt.Sprintf("attachment %d (%s) on %s#%d", item.ID, item.Name, t.Repo, n)
|
|
}
|
|
|
|
func attachmentRow(item services.IssueAttachment) []terminal.Cell {
|
|
return []terminal.Cell{
|
|
{Text: strconv.FormatInt(item.ID, 10), Link: item.URL},
|
|
{Text: item.Name},
|
|
{Text: terminal.Bytes(item.Size)},
|
|
{Text: strconv.FormatInt(item.DownloadCount, 10)},
|
|
{Time: terminal.ParseTime(item.CreatedAt)},
|
|
}
|
|
}
|
|
|
|
func attachmentView(item services.IssueAttachment) terminal.View {
|
|
return terminal.View{
|
|
Title: item.Name, ID: "#" + strconv.FormatInt(item.ID, 10),
|
|
Fields: []terminal.Field{
|
|
{Label: "Size", Value: terminal.Bytes(item.Size)},
|
|
{Label: "Downloads", Value: strconv.FormatInt(item.DownloadCount, 10)},
|
|
{Label: "Created", Value: item.CreatedAt},
|
|
},
|
|
Kind: "attachment", URL: item.URL,
|
|
}
|
|
}
|
|
|
|
func (a *application) issueAttachmentCommand(kind refKind) *cobra.Command {
|
|
words := commentWords(kind)
|
|
group := &cobra.Command{Use: "attachment", Short: "Manage the attachments of " + kind.noun() + "s", Long: fmt.Sprintf(`List, view, upload, download, rename and delete the files attached to %s. Attachments
|
|
are addressed by the %s number and the attachment ID that attachment list shows.`, kind.article(), kind.noun())}
|
|
annotations := map[string]string{"tea.arguments": words.arguments}
|
|
// discussionTarget resolves the issue or pull request argument of an attachment command.
|
|
discussionTarget := func(cmd *cobra.Command, raw string) (target, int64, error) {
|
|
t, n, err := a.resolveRef(cmd, raw, kind)
|
|
if err == nil && kind == pullRef {
|
|
err = requirePull(cmd, t, n)
|
|
}
|
|
return t, n, err
|
|
}
|
|
completeFirst := a.completeDiscussions(kind)
|
|
|
|
var limit, page int
|
|
list := &cobra.Command{Use: "list {<number> | <url>}", Short: "List attachments", Example: fmt.Sprintf("$ tea %s attachment list %s", words.group, words.example), Annotations: annotations, Args: argsExact(1), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
|
|
t, n, err := discussionTarget(cmd, args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items, err := (services.Issues{Client: t.client, Repository: t.Repo}).Attachments(cmd.Context(), n, limit, page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
where := fmt.Sprintf("on %s#%d", t.Repo, n)
|
|
return writeTable(a, cmd, items, listSpec[services.IssueAttachment]{
|
|
Columns: []terminal.Column{{Header: "ID"}, {Header: "Name", Flex: true}, {Header: "Size"}, {Header: "Downloads"}, {Header: "Created"}},
|
|
Row: attachmentRow, Empty: "no attachments " + where, Context: "attachments " + where,
|
|
}, listPage{Number: page})
|
|
}}
|
|
pageFlags(list, &limit, &page)
|
|
|
|
view := &cobra.Command{Use: "view {<number> | <url>} <attachment-id>", Short: "View an attachment", Example: fmt.Sprintf("$ tea %s attachment view %s 9", words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
|
|
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
item, err := (services.Issues{Client: t.client, Repository: t.Repo}).Attachment(cmd.Context(), n, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeView(a, cmd, item, attachmentView)
|
|
}}
|
|
|
|
var uploadName string
|
|
upload := &cobra.Command{Use: "upload {<number> | <url>} <file>", Short: "Attach a file", Example: fmt.Sprintf(`$ tea %[1]s attachment upload %[2]s crash.log
|
|
$ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: pathArgs(1, false, completeFiles, completeFirst), RunE: func(cmd *cobra.Command, args []string) error {
|
|
name := uploadName
|
|
if cmd.Flags().Changed("name") && strings.TrimSpace(name) == "" {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--name cannot be empty"))
|
|
}
|
|
if name == "" {
|
|
name = filepath.Base(args[1])
|
|
}
|
|
files, err := openRegularFiles(args[1:])
|
|
defer closeFiles(files)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t, n, err := discussionTarget(cmd, args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
item, err := (services.Issues{Client: t.client, Repository: t.Repo}).UploadAttachment(cmd.Context(), n, name, files[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.done(cmd, "Uploaded %s (%s) to %s#%d", item.Name, terminal.Bytes(item.Size), t.Repo, n)
|
|
return a.created(cmd, item, item.URL)
|
|
}}
|
|
upload.Flags().StringVar(&uploadName, "name", "", "File `name` of the attachment (default: the local file name)")
|
|
|
|
var newName string
|
|
edit := &cobra.Command{Use: "edit {<number> | <url>} <attachment-id>", Short: "Rename an attachment", Example: fmt.Sprintf("$ tea %s attachment edit %s 9 --name crash-2026-10-11.log", words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
|
|
if strings.TrimSpace(newName) == "" {
|
|
return clierr.Wrap(clierr.Usage, errors.New("supply a nonempty --name"))
|
|
}
|
|
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
item, err := (services.Issues{Client: t.client, Repository: t.Repo}).RenameAttachment(cmd.Context(), n, id, newName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.done(cmd, "Renamed %s", attachmentName(t, n, item))
|
|
return a.jsonResult(cmd, item)
|
|
}}
|
|
edit.Flags().StringVar(&newName, "name", "", "New file `name`")
|
|
|
|
remove := &cobra.Command{Use: "delete {<number> | <url>} <attachment-id>", Short: "Delete an attachment", Example: fmt.Sprintf("$ tea %s attachment delete %s 9 --yes", words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.confirm": "true", "tea.text": "attachment deletion does not return a resource"}, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
|
|
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
service := services.Issues{Client: t.client, Repository: t.Repo}
|
|
item, err := service.Attachment(cmd.Context(), n, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := attachmentName(t, n, item)
|
|
return a.deleteConfirmed(cmd, name, func() error { return service.DeleteAttachment(cmd.Context(), n, id) })
|
|
}}
|
|
|
|
var destination downloadFlags
|
|
download := &cobra.Command{Use: "download {<number> | <url>} <attachment-id>", Short: "Download an attachment", Long: `Download an attachment into the current directory under its own name, or where -O/--output
|
|
or -D/--dir say. An existing file is kept unless --clobber replaces it or --skip-existing
|
|
skips the download.`, Example: fmt.Sprintf(`$ tea %[1]s attachment download %[2]s 9
|
|
$ tea %[1]s attachment download %[2]s 9 --output crash.log --clobber
|
|
# Write the file to standard output
|
|
$ tea %[1]s attachment download %[2]s 9 -O - | less`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.text": "attachment downloads are binary"}, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
|
|
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
item, body, err := (services.Issues{Client: t.client, Repository: t.Repo}).DownloadAttachment(cmd.Context(), n, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer body.Close()
|
|
path, skip, err := destination.destination(item.Name)
|
|
switch {
|
|
case err != nil:
|
|
return err
|
|
case skip:
|
|
a.warn(cmd, "Skipped %s: %s exists", attachmentName(t, n, item), path)
|
|
return nil
|
|
}
|
|
if err := a.download(cmd, destination, path, body); err != nil {
|
|
return err
|
|
}
|
|
if path != "-" {
|
|
a.done(cmd, "Downloaded %s (%s) to %s", item.Name, terminal.Bytes(item.Size), path)
|
|
}
|
|
return nil
|
|
}}
|
|
destination.register(download)
|
|
group.AddCommand(list, view, upload, edit, remove, download)
|
|
return group
|
|
}
|
|
|
|
// attachmentArgs resolves the issue or pull request and attachment ID arguments of an attachment
|
|
// command, checking the ID before any request.
|
|
func (a *application) attachmentArgs(cmd *cobra.Command, args []string, resolve func(*cobra.Command, string) (target, int64, error)) (target, int64, int64, error) {
|
|
id, err := number(args[1])
|
|
if err != nil {
|
|
return target{}, 0, 0, err
|
|
}
|
|
t, n, err := resolve(cmd, args[0])
|
|
return t, n, id, err
|
|
}
|