Files
gitea-tea/internal/app/issue_attachments.go
T
bircni 2826726557 refactor(issue,pr): trim help, restating comments and single-use helpers
Cut issue, pr, label, milestone and comment examples to at most three, drop
doc comments that only restate the name, fold the comment and attachment
argument lookups into one closure each, and inline label helpers with one
caller.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-12 20:48:00 +02:00

194 lines
8.3 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 := 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
}
// attachmentArgs checks the attachment ID before any request.
attachmentArgs := func(cmd *cobra.Command, args []string) (target, int64, int64, error) {
id, err := number(args[1])
if err != nil {
return target{}, 0, 0, err
}
t, n, err := discussionTarget(cmd, args[0])
return t, n, id, 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 := attachmentArgs(cmd, args)
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 := attachmentArgs(cmd, args)
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 := attachmentArgs(cmd, args)
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
}
return a.deleteConfirmed(cmd, attachmentName(t, n, item), 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 := attachmentArgs(cmd, args)
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
}