mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
feat(release): show notes and assets, upload with --clobber, download like gh
release view printed "v0.1.0 v0.1.0" and a URL: the notes and assets were only in --json. It now shows the title and tag, a Latest, Pre-release or Draft badge, who released it when and the target, the assets with their sizes and the notes, rendered on a terminal. Without a tag it shows the latest release, and --web opens it. release asset upload silently attached a second asset with the same name; it now refuses unless --clobber replaces the existing one. release delete printed the deleted release as a live row; it reports "✓ Deleted release v2.0.0 ... (tag kept; use --delete-tag)". release list shows TITLE, TYPE, TAG and PUBLISHED. release create takes asset files after the tag, -t/-n/-F/-e/-d/-p (--body and --body-file still work) and the new alias. Downloads select assets by name or ID or with --pattern, default to every asset of the latest release, write to ./NAME, --dir or --output with --clobber/--skip-existing, and --archive fetches the source code. release upload and release download are shortcuts for the asset commands, asset edit and delete take names or IDs, and the release JSON gains author and assets. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
11 files changed
+984
-357
No files matched your search
+368
-140
@@ -4,9 +4,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -19,78 +22,103 @@ import (
|
||||
)
|
||||
|
||||
func (a *application) releaseAssetCommand() *cobra.Command {
|
||||
group := &cobra.Command{Use: "asset", Short: "Manage release assets"}
|
||||
var limit, page int
|
||||
list := &cobra.Command{Use: "list <tag>", Short: "List release assets", Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := services.Releases{Client: target.client, Repository: target.Repo}
|
||||
release, err := service.View(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := service.Assets(cmd.Context(), release.ID, limit, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeList(a, cmd, items, assetRow)
|
||||
}}
|
||||
pageFlags(list, &limit, &page)
|
||||
group.AddCommand(list)
|
||||
var name string
|
||||
upload := &cobra.Command{Use: "upload <tag> <file>...", Short: "Upload release assets", Args: usageArgs(cobra.MinimumNArgs(2)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("name") && (len(args) != 2 || strings.TrimSpace(name) == "") {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("--name requires exactly one file and a nonempty name"))
|
||||
}
|
||||
files, err := openRegularFiles(args[1:])
|
||||
defer closeFiles(files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := services.Releases{Client: target.client, Repository: target.Repo}
|
||||
release, err := service.View(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := func(file *os.File) string {
|
||||
if cmd.Flags().Changed("name") {
|
||||
return name
|
||||
}
|
||||
return filepath.Base(file.Name())
|
||||
}
|
||||
return batch(a, cmd, files, func(file *os.File) string { return "upload asset " + filename(file) }, func(file *os.File) (services.ReleaseAsset, error) {
|
||||
return service.UploadAsset(cmd.Context(), release.ID, filename(file), file)
|
||||
}, assetRow)
|
||||
}}
|
||||
group := &cobra.Command{
|
||||
Use: "asset",
|
||||
Short: "Manage release assets",
|
||||
Long: "List, upload, download, rename and delete the files attached to a release. Assets are named by name or ID.",
|
||||
}
|
||||
group.AddCommand(a.assetListCommand(), a.releaseUploadCommand(), a.releaseDownloadCommand(), a.assetEditCommand(), a.assetDeleteCommand())
|
||||
return group
|
||||
}
|
||||
|
||||
upload.Flags().StringVar(&name, "name", "", "Uploaded asset name (defaults to filename)")
|
||||
group.AddCommand(upload)
|
||||
for _, verb := range []string{"edit", "delete"} {
|
||||
var newName string
|
||||
var byName bool
|
||||
command := &cobra.Command{Use: verb + " <tag> <asset-id>", Short: titleCase(verb) + " release assets", Args: argsExact(2)}
|
||||
if verb == "delete" {
|
||||
command.Use = "delete <tag> <asset-id>..."
|
||||
command.Args = usageArgs(cobra.MinimumNArgs(2))
|
||||
command.Flags().BoolVar(&byName, "by-name", false, "Select assets by exact name instead of ID")
|
||||
command.Annotations = textOutput("asset deletion does not return a resource")
|
||||
// assetFinder addresses the assets of the release tagged tag by name or ID.
|
||||
func assetFinder(tag string, byName bool) services.NameOrID[services.ReleaseAsset] {
|
||||
return services.NameOrID[services.ReleaseAsset]{
|
||||
Kind: "asset", Scope: "release " + tag, ByName: byName,
|
||||
ID: func(asset services.ReleaseAsset) int64 { return asset.ID }, Name: func(asset services.ReleaseAsset) string { return asset.Name },
|
||||
}
|
||||
}
|
||||
|
||||
// completeReleaseAssets completes a release tag, then the asset names of the release it tags.
|
||||
func (a *application) completeReleaseAssets() cobra.CompletionFunc {
|
||||
releases := a.completeReleases()
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return releases(cmd, args, toComplete)
|
||||
}
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
if verb == "edit" && strings.TrimSpace(newName) == "" {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("supply --name for asset rename"))
|
||||
tag := args[0]
|
||||
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
|
||||
release, err := services.Releases{Client: t.client, Repository: t.Repo}.View(ctx, tag)
|
||||
return describe(release.Assets, func(asset services.ReleaseAsset) (string, string) { return asset.Name, terminal.Bytes(asset.Size) }), err
|
||||
})(cmd, args, toComplete)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *application) assetListCommand() *cobra.Command {
|
||||
var limit, page int
|
||||
command := &cobra.Command{
|
||||
Use: "list <tag>",
|
||||
Short: "List release assets",
|
||||
Example: "$ tea release asset list v1.0.0",
|
||||
Args: argsExact(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ids []int64
|
||||
if !byName {
|
||||
var err error
|
||||
if ids, err = resourceNumbers(args[1:]); err != nil {
|
||||
return err
|
||||
service := services.Releases{Client: target.client, Repository: target.Repo}
|
||||
release, err := service.View(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := service.Assets(cmd.Context(), release.ID, limit, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spec := listSpec[services.ReleaseAsset]{
|
||||
Columns: []terminal.Column{{Header: "ID"}, {Header: "Name", Flex: true}, {Header: "Size"}, {Header: "Downloads"}, {Header: "Created"}, {Header: "URL", PipedOnly: true}},
|
||||
Row: func(item services.ReleaseAsset) []terminal.Cell {
|
||||
size := terminal.Bytes(item.Size)
|
||||
if !a.deps.Streams.OutTTY {
|
||||
size = strconv.FormatInt(item.Size, 10)
|
||||
}
|
||||
return []terminal.Cell{{Text: strconv.FormatInt(item.ID, 10), Link: item.URL}, {Text: item.Name}, {Text: size}, {Text: strconv.FormatInt(item.DownloadCount, 10)}, {Time: terminal.ParseTime(item.CreatedAt)}, {Text: item.URL}}
|
||||
},
|
||||
Context: "assets of release " + release.Tag + " in " + target.Repo, Empty: "no assets in release " + release.Tag + " of " + target.Repo,
|
||||
}
|
||||
return writeTable(a, cmd, items, spec, listPage{Number: page, More: len(release.Assets) > page*limit})
|
||||
},
|
||||
}
|
||||
pageFlags(command, &limit, &page)
|
||||
command.ValidArgsFunction = a.completeReleases()
|
||||
return command
|
||||
}
|
||||
|
||||
func (a *application) releaseUploadCommand() *cobra.Command {
|
||||
var name string
|
||||
var clobber bool
|
||||
command := &cobra.Command{
|
||||
Use: "upload <tag> <file>...",
|
||||
Short: "Upload release assets",
|
||||
Long: `Upload files as assets of a release, named after the files unless --name renames a single file.
|
||||
An asset with the same name is replaced only with --clobber.`,
|
||||
Example: `$ tea release upload v1.0.0 dist/tea-linux dist/tea-darwin
|
||||
$ tea release upload v1.0.0 dist/tea-linux --clobber`,
|
||||
Args: usageArgs(cobra.MinimumNArgs(2)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("name") && (len(args) != 2 || strings.TrimSpace(name) == "") {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("--name requires exactly one file and a nonempty name"))
|
||||
}
|
||||
files, err := openRegularFiles(args[1:])
|
||||
defer closeFiles(files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := func(file *os.File) string {
|
||||
if cmd.Flags().Changed("name") {
|
||||
return name
|
||||
}
|
||||
return filepath.Base(file.Name())
|
||||
}
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
@@ -101,87 +129,287 @@ func (a *application) releaseAssetCommand() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if verb == "edit" {
|
||||
item, err := service.RenameAsset(cmd.Context(), release.ID, ids[0], newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeItem(a, cmd, item, assetRow)
|
||||
}
|
||||
if byName {
|
||||
assets, err := service.AllAssets(cmd.Context(), release.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range args[1:] {
|
||||
var matches []int64
|
||||
for _, asset := range assets {
|
||||
if asset.Name == name {
|
||||
matches = append(matches, asset.ID)
|
||||
replaced := map[string]services.ReleaseAsset{}
|
||||
for _, file := range files {
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == filename(file) {
|
||||
if !clobber {
|
||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("asset %s already exists in release %s; pass --clobber to replace it", asset.Name, release.Tag))
|
||||
}
|
||||
replaced[asset.Name] = asset
|
||||
}
|
||||
if len(matches) != 1 {
|
||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("asset name %q matches %d assets; use an explicit asset ID", name, len(matches)))
|
||||
}
|
||||
if slices.Contains(ids, matches[0]) {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("duplicate asset selections"))
|
||||
}
|
||||
ids = append(ids, matches[0])
|
||||
}
|
||||
}
|
||||
if err := a.confirm(cmd, "Delete release assets "+strings.Join(args[1:], ", ")+" on "+target.Repo); err != nil {
|
||||
return batchDone(a, cmd, files, func(file *os.File) string { return "upload asset " + filename(file) }, func(file *os.File) (services.ReleaseAsset, error) {
|
||||
if old, ok := replaced[filename(file)]; ok {
|
||||
if err := service.DeleteAsset(cmd.Context(), release.ID, old.ID); err != nil {
|
||||
return services.ReleaseAsset{}, fmt.Errorf("replace asset %s: %w", old.Name, err)
|
||||
}
|
||||
}
|
||||
return service.UploadAsset(cmd.Context(), release.ID, filename(file), file)
|
||||
}, func(asset services.ReleaseAsset) string {
|
||||
return fmt.Sprintf("Uploaded %s (%s) to release %s", asset.Name, terminal.Bytes(asset.Size), release.Tag)
|
||||
})
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&name, "name", "", "Asset name (default: the file's name)")
|
||||
command.Flags().BoolVar(&clobber, "clobber", false, "Replace assets with the same name")
|
||||
command.ValidArgsFunction = a.completeReleases()
|
||||
return command
|
||||
}
|
||||
|
||||
func (a *application) releaseDownloadCommand() *cobra.Command {
|
||||
var flags downloadFlags
|
||||
var patterns []string
|
||||
var archive string
|
||||
command := &cobra.Command{
|
||||
Use: "download [<tag>] [<asset>...]",
|
||||
Short: "Download release assets",
|
||||
Long: `Download assets of a release, by name or ID, or those matching --pattern; without either, every
|
||||
asset. --archive downloads the source code archive instead. Without a tag, the latest release is used.
|
||||
|
||||
Files are written to the current directory under their names, to --dir, or to --output; an existing
|
||||
file is replaced only with --clobber. --output - writes one asset to standard output.`,
|
||||
Example: `# Download every asset of the latest release
|
||||
$ tea release download
|
||||
|
||||
# Download the Linux builds of a release into dist/
|
||||
$ tea release download v1.0.0 --pattern '*linux*' --dir dist
|
||||
|
||||
# Download the source code as a zip file
|
||||
$ tea release download v1.0.0 --archive zip`,
|
||||
Annotations: textOutput("asset downloads are files"),
|
||||
Args: usageArgs(cobra.ArbitraryArgs),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
for _, pattern := range patterns {
|
||||
if _, err := path.Match(pattern, ""); err != nil {
|
||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("invalid --pattern %q", pattern))
|
||||
}
|
||||
}
|
||||
tag, names := "", []string{}
|
||||
if len(args) > 0 {
|
||||
tag, names = args[0], args[1:]
|
||||
}
|
||||
if archive != "" && (len(names) > 0 || len(patterns) > 0) {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("--archive downloads the source code only; it cannot be combined with assets or --pattern"))
|
||||
}
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var failures []error
|
||||
for _, id := range ids {
|
||||
if err := service.DeleteAsset(cmd.Context(), release.ID, id); err != nil {
|
||||
failures = append(failures, fmt.Errorf("delete asset %d: %w", id, err))
|
||||
continue
|
||||
}
|
||||
if len(ids) > 1 {
|
||||
if _, err := fmt.Fprintf(cmd.ErrOrStderr(), "Deleted asset %d\n", id); err != nil {
|
||||
return err
|
||||
}
|
||||
service := services.Releases{Client: target.client, Repository: target.Repo}
|
||||
release, err := viewRelease(cmd, service, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if archive != "" {
|
||||
return a.downloadArchive(cmd, service, flags, release.Tag, archive)
|
||||
}
|
||||
assets, err := selectAssets(release, names, patterns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(assets) > 1 && flags.output != "" && !isDirectory(flags.output) {
|
||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("--output names one file, but %d assets match; pass --dir", len(assets)))
|
||||
}
|
||||
paths := make([]string, len(assets))
|
||||
skipped := make([]bool, len(assets))
|
||||
for i, asset := range assets {
|
||||
if paths[i], skipped[i], err = flags.destination(asset.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
if verb == "edit" {
|
||||
command.Flags().StringVar(&newName, "name", "", "New asset name")
|
||||
}
|
||||
group.AddCommand(command)
|
||||
for i, asset := range assets {
|
||||
if skipped[i] {
|
||||
a.warn(cmd, "Skipped %s; %s already exists", asset.Name, paths[i])
|
||||
continue
|
||||
}
|
||||
body, err := service.DownloadAsset(cmd.Context(), release.ID, asset.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download %s: %w", asset.Name, err)
|
||||
}
|
||||
err = a.download(cmd, flags, paths[i], body)
|
||||
_ = body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if paths[i] != "-" {
|
||||
a.done(cmd, "Downloaded %s (%s) to %s", asset.Name, terminal.Bytes(asset.Size), paths[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var output string
|
||||
download := &cobra.Command{Use: "download <tag> <asset-id>", Short: "Download an asset to a file or stdout", Annotations: textOutput("asset downloads are binary"), Args: argsExact(2), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
id, err := number(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDownloadDestination(output); err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := services.Releases{Client: target.client, Repository: target.Repo}
|
||||
release, err := service.View(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := service.DownloadAsset(cmd.Context(), release.ID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
return writeDownload(cmd, body, output)
|
||||
}}
|
||||
download.Flags().StringVar(&output, "output-file", "", "Destination file (must not exist), or - for stdout")
|
||||
group.AddCommand(download)
|
||||
return group
|
||||
flags.register(command)
|
||||
command.Flags().StringSliceVarP(&patterns, "pattern", "p", nil, "Download the assets whose names match this glob (repeatable)")
|
||||
enumFlag(command, &archive, "archive", "A", "", "Download the source code archive", "zip", "tar.gz")
|
||||
command.ValidArgsFunction = a.completeReleaseAssets()
|
||||
return command
|
||||
}
|
||||
|
||||
func assetRow(item services.ReleaseAsset) []terminal.Cell {
|
||||
return terminal.Row(strconv.FormatInt(item.ID, 10), item.Name, strconv.FormatInt(item.Size, 10), item.URL)
|
||||
// selectAssets returns the assets of release that names (names or IDs) and patterns (globs)
|
||||
// select, or every asset when there are neither, each once.
|
||||
func selectAssets(release services.Release, names, patterns []string) ([]services.ReleaseAsset, error) {
|
||||
if len(names) == 0 && len(patterns) == 0 {
|
||||
if len(release.Assets) == 0 {
|
||||
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("release %s has no assets; pass --archive to download its source code", release.Tag))
|
||||
}
|
||||
return release.Assets, nil
|
||||
}
|
||||
var selected []services.ReleaseAsset
|
||||
add := func(asset services.ReleaseAsset) {
|
||||
if !slices.ContainsFunc(selected, func(other services.ReleaseAsset) bool { return other.ID == asset.ID }) {
|
||||
selected = append(selected, asset)
|
||||
}
|
||||
}
|
||||
finder := assetFinder(release.Tag, false)
|
||||
for _, name := range names {
|
||||
asset, err := finder.Find(release.Assets, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
add(asset)
|
||||
}
|
||||
for _, pattern := range patterns {
|
||||
matched := false
|
||||
for _, asset := range release.Assets {
|
||||
if ok, _ := path.Match(pattern, asset.Name); ok {
|
||||
add(asset)
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return nil, clierr.Wrap(clierr.Usage, fmt.Errorf("no asset of release %s matches %q", release.Tag, pattern))
|
||||
}
|
||||
}
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
// downloadArchive downloads the source code of the tag as a zip or tar.gz file.
|
||||
func (a *application) downloadArchive(cmd *cobra.Command, service services.Releases, flags downloadFlags, tag, format string) error {
|
||||
_, repo, _ := strings.Cut(service.Repository, "/")
|
||||
name := repo + "-" + strings.ReplaceAll(tag, "/", "-") + "." + format
|
||||
destination, skip, err := flags.destination(name)
|
||||
if err != nil || skip {
|
||||
if skip {
|
||||
a.warn(cmd, "Skipped %s; %s already exists", name, destination)
|
||||
}
|
||||
return err
|
||||
}
|
||||
body, err := service.Archive(cmd.Context(), tag, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
counter := &countingReader{reader: body}
|
||||
if err := a.download(cmd, flags, destination, counter); err != nil {
|
||||
return err
|
||||
}
|
||||
if destination != "-" {
|
||||
a.done(cmd, "Downloaded %s (%s) to %s", name, terminal.Bytes(counter.n), destination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// countingReader counts the bytes read through it.
|
||||
type countingReader struct {
|
||||
reader io.Reader
|
||||
n int64
|
||||
}
|
||||
|
||||
func (r *countingReader) Read(p []byte) (int, error) {
|
||||
n, err := r.reader.Read(p)
|
||||
r.n += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (a *application) assetEditCommand() *cobra.Command {
|
||||
var name string
|
||||
command := &cobra.Command{
|
||||
Use: "edit <tag> <asset>",
|
||||
Short: "Rename a release asset",
|
||||
Example: "$ tea release asset edit v1.0.0 tea-linux --name tea-linux-amd64",
|
||||
Args: argsExact(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return clierr.Wrap(clierr.Usage, errors.New("supply --name for asset rename"))
|
||||
}
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := services.Releases{Client: target.client, Repository: target.Repo}
|
||||
release, err := service.View(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
asset, err := assetFinder(release.Tag, false).Find(release.Assets, args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item, err := service.RenameAsset(cmd.Context(), release.ID, asset.ID, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.done(cmd, "Renamed asset %s to %s in release %s", asset.Name, item.Name, release.Tag)
|
||||
return a.writeMutationJSON(cmd, item)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&name, "name", "", "New asset name")
|
||||
command.ValidArgsFunction = a.completeReleaseAssets()
|
||||
return command
|
||||
}
|
||||
|
||||
func (a *application) assetDeleteCommand() *cobra.Command {
|
||||
var byName bool
|
||||
command := &cobra.Command{
|
||||
Use: "delete <tag> <asset>...",
|
||||
Short: "Delete release assets",
|
||||
Example: `$ tea release asset delete v1.0.0 tea-linux
|
||||
$ tea release asset delete v1.0.0 12 13 --yes`,
|
||||
Annotations: map[string]string{"tea.confirm": "true", "tea.text": "asset deletion does not return a resource"},
|
||||
Args: usageArgs(cobra.MinimumNArgs(2)),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
target, err := a.resolve(cmd.Context(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := services.Releases{Client: target.client, Repository: target.Repo}
|
||||
release, err := service.View(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
finder := assetFinder(release.Tag, byName)
|
||||
var assets []services.ReleaseAsset
|
||||
for _, raw := range args[1:] {
|
||||
asset, err := finder.Find(release.Assets, raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if slices.ContainsFunc(assets, func(other services.ReleaseAsset) bool { return other.ID == asset.ID }) {
|
||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("asset %s is selected more than once", asset.Name))
|
||||
}
|
||||
assets = append(assets, asset)
|
||||
}
|
||||
names := make([]string, len(assets))
|
||||
for i, asset := range assets {
|
||||
names[i] = asset.Name
|
||||
}
|
||||
subject := "asset " + names[0]
|
||||
if len(names) > 1 {
|
||||
subject = fmt.Sprintf("%d assets (%s)", len(names), strings.Join(names, ", "))
|
||||
}
|
||||
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Delete " + subject + " from release " + release.Tag + " in " + target.Repo + "?", NonTTY: "delete " + subject}); err != nil {
|
||||
return err
|
||||
}
|
||||
return batchDone(a, cmd, assets, func(asset services.ReleaseAsset) string { return "delete asset " + asset.Name }, func(asset services.ReleaseAsset) (services.ReleaseAsset, error) {
|
||||
return asset, service.DeleteAsset(cmd.Context(), release.ID, asset.ID)
|
||||
}, func(asset services.ReleaseAsset) string {
|
||||
return fmt.Sprintf("Deleted asset %s (ID %d) from release %s", asset.Name, asset.ID, release.Tag)
|
||||
})
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&byName, "by-name", false, "Match assets by name only, also when numeric")
|
||||
command.ValidArgsFunction = a.completeReleaseAssets()
|
||||
return command
|
||||
}
|
||||
Reference in new issue
Block a user