create cache commands
This commit is contained in:
parent
50e16890f8
commit
213a59b8bd
8 changed files with 980 additions and 0 deletions
29
pkg/cmd/cache/cache.go
vendored
Normal file
29
pkg/cmd/cache/cache.go
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
cmdDelete "github.com/cli/cli/v2/pkg/cmd/cache/delete"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/cache/list"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdCache(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cache <command>",
|
||||
Short: "Manage Github Actions caches",
|
||||
Long: "Work with Github Actions caches.",
|
||||
Example: heredoc.Doc(`
|
||||
$ gh cache list
|
||||
$ gh cache delete --all
|
||||
`),
|
||||
GroupID: "actions",
|
||||
}
|
||||
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
155
pkg/cmd/cache/delete/delete.go
vendored
Normal file
155
pkg/cmd/cache/delete/delete.go
vendored
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type DeleteOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
DeleteAll bool
|
||||
Identifier string
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
|
||||
opts := &DeleteOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [<cache-id>| <cache--key> | --all]",
|
||||
Short: "Delete Github Actions caches",
|
||||
Long: `
|
||||
Delete Github Actions caches.
|
||||
|
||||
Deletion requires authorization with the "repo" scope.
|
||||
`,
|
||||
Example: heredoc.Doc(`
|
||||
# Delete a cache by id
|
||||
$ gh cache delete 1234
|
||||
|
||||
# Delete a cache by key
|
||||
$ gh cache delete cache-key
|
||||
|
||||
# Delete a cache by id in a specific repo
|
||||
$ gh cache delete 1234 --repo cli/cli
|
||||
|
||||
# Delete all caches
|
||||
$ gh cache delete --all
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support -R/--repo flag
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"specify only one of cache id, cache key, or --all",
|
||||
opts.DeleteAll, len(args) > 0,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.DeleteAll && len(args) == 0 {
|
||||
return cmdutil.FlagErrorf("must provide either cache id, cache key, or use --all")
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
opts.Identifier = args[0]
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return deleteRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deleteRun(opts *DeleteOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create http client: %w", err)
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine base repo: %w", err)
|
||||
}
|
||||
|
||||
var toDelete []string
|
||||
if opts.DeleteAll {
|
||||
caches, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: -1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(caches.ActionsCaches) == 0 {
|
||||
return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon())
|
||||
}
|
||||
for _, cache := range caches.ActionsCaches {
|
||||
toDelete = append(toDelete, strconv.Itoa(cache.Id))
|
||||
}
|
||||
} else {
|
||||
toDelete = append(toDelete, opts.Identifier)
|
||||
}
|
||||
|
||||
return deleteCaches(opts, client, repo, toDelete)
|
||||
}
|
||||
|
||||
func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface, toDelete []string) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
repoName := ghrepo.FullName(repo)
|
||||
opts.IO.StartProgressIndicator()
|
||||
base := fmt.Sprintf("repos/%s/actions/caches", repoName)
|
||||
|
||||
for _, cache := range toDelete {
|
||||
path := ""
|
||||
if id, err := strconv.Atoi(cache); err == nil {
|
||||
path = fmt.Sprintf("%s/%d", base, id)
|
||||
} else {
|
||||
path = fmt.Sprintf("%s?key=%s", base, cache)
|
||||
}
|
||||
|
||||
err := client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
|
||||
if err != nil {
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
if httpErr.StatusCode == http.StatusNotFound {
|
||||
err = fmt.Errorf("%s Could not find a cache matching %s in %s", cs.FailureIcon(), cache, repoName)
|
||||
} else {
|
||||
err = fmt.Errorf("%s Failed to delete cache: %w", cs.FailureIcon(), err)
|
||||
}
|
||||
}
|
||||
opts.IO.StopProgressIndicator()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.IO.StopProgressIndicator()
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.Out, "%s Deleted %s from %s\n", cs.SuccessIcon(), text.Pluralize(len(toDelete), "cache"), repoName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
209
pkg/cmd/cache/delete/delete_test.go
vendored
Normal file
209
pkg/cmd/cache/delete/delete_test.go
vendored
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants DeleteOptions
|
||||
wantsErr string
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
cli: "",
|
||||
wantsErr: "must provide either cache id, cache key, or use --all",
|
||||
},
|
||||
{
|
||||
name: "id argument",
|
||||
cli: "123",
|
||||
wants: DeleteOptions{Identifier: "123"},
|
||||
},
|
||||
{
|
||||
name: "key argument",
|
||||
cli: "A-Cache-Key",
|
||||
wants: DeleteOptions{Identifier: "A-Cache-Key"},
|
||||
},
|
||||
{
|
||||
name: "delete all flag",
|
||||
cli: "--all",
|
||||
wants: DeleteOptions{DeleteAll: true},
|
||||
},
|
||||
{
|
||||
name: "id argument and delete all flag",
|
||||
cli: "1 --all",
|
||||
wantsErr: "specify only one of cache id, cache key, or --all",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
var gotOpts *DeleteOptions
|
||||
cmd := NewCmdDelete(f, func(opts *DeleteOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr != "" {
|
||||
assert.EqualError(t, err, tt.wantsErr)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll)
|
||||
assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts DeleteOptions
|
||||
stubs func(*httpmock.Registry)
|
||||
tty bool
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
wantStderr string
|
||||
wantStdout string
|
||||
}{
|
||||
{
|
||||
name: "deletes cache tty",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
},
|
||||
tty: true,
|
||||
wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "deletes cache notty",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
},
|
||||
tty: false,
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "non-existent cache",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(404, ""),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "X Could not find a cache matching 123 in OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "deletes all caches",
|
||||
opts: DeleteOptions{DeleteAll: true},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{
|
||||
{
|
||||
Id: 123,
|
||||
Key: "foo",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
},
|
||||
{
|
||||
Id: 456,
|
||||
Key: "bar",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/456"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
},
|
||||
tty: true,
|
||||
wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "displays delete error",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "X Failed to delete cache: HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches/123)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.stubs != nil {
|
||||
tt.stubs(reg)
|
||||
}
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
defer reg.Verify(t)
|
||||
|
||||
err := deleteRun(&tt.opts)
|
||||
if tt.wantErr {
|
||||
if tt.wantErrMsg != "" {
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
148
pkg/cmd/cache/list/list.go
vendored
Normal file
148
pkg/cmd/cache/list/list.go
vendored
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ListOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
Now time.Time
|
||||
|
||||
Limit int
|
||||
Order string
|
||||
Sort string
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
opts := ListOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List Github Actions caches",
|
||||
Example: heredoc.Doc(`
|
||||
# List caches for current repository
|
||||
$ gh cache list
|
||||
|
||||
# List caches for specific repository
|
||||
$ gh cache list --repo cli/cli
|
||||
|
||||
# List caches sorted by least recently accessed
|
||||
$ gh cache list --sort last_accessed_at --order asc
|
||||
`),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if opts.Limit < 1 {
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(&opts)
|
||||
}
|
||||
|
||||
return listRun(&opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of caches to fetch")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Order, "order", "O", "desc", []string{"asc", "desc"}, "Order of caches returned")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Sort, "sort", "S", "last_accessed_at", []string{"created_at", "last_accessed_at", "size_in_bytes"}, "Sort fetched caches")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
result, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: opts.Limit, Sort: opts.Sort, Order: opts.Order})
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Failed to get caches: %w", opts.IO.ColorScheme().FailureIcon(), err)
|
||||
}
|
||||
|
||||
if len(result.ActionsCaches) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("No caches found in %s", ghrepo.FullName(repo)))
|
||||
}
|
||||
|
||||
if err := opts.IO.StartPager(); err == nil {
|
||||
defer opts.IO.StopPager()
|
||||
} else {
|
||||
fmt.Fprintf(opts.IO.Out, "Failed to start pager: %v\n", err)
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.Out, "\nShowing %d of %s in %s\n\n", len(result.ActionsCaches), text.Pluralize(result.TotalCount, "cache"), ghrepo.FullName(repo))
|
||||
}
|
||||
|
||||
if opts.Now.IsZero() {
|
||||
opts.Now = time.Now()
|
||||
}
|
||||
|
||||
tp := tableprinter.New(opts.IO)
|
||||
tp.HeaderRow("ID", "KEY", "SIZE", "CREATED", "ACCESSED")
|
||||
for _, cache := range result.ActionsCaches {
|
||||
tp.AddField(opts.IO.ColorScheme().Cyan(fmt.Sprintf("%d", cache.Id)))
|
||||
tp.AddField(cache.Key)
|
||||
tp.AddField(humanFileSize(cache.SizeInBytes))
|
||||
tp.AddTimeField(time.Now(), cache.CreatedAt, opts.IO.ColorScheme().Gray)
|
||||
tp.AddTimeField(time.Now(), cache.LastAccessedAt, opts.IO.ColorScheme().Gray)
|
||||
tp.EndRow()
|
||||
}
|
||||
|
||||
return tp.Render()
|
||||
}
|
||||
|
||||
func humanFileSize(s int64) string {
|
||||
if s < 1024 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
|
||||
kb := float64(s) / 1024
|
||||
if kb < 1024 {
|
||||
return fmt.Sprintf("%s KiB", floatToString(kb, 2))
|
||||
}
|
||||
|
||||
mb := kb / 1024
|
||||
if mb < 1024 {
|
||||
return fmt.Sprintf("%s MiB", floatToString(mb, 2))
|
||||
}
|
||||
|
||||
gb := mb / 1024
|
||||
return fmt.Sprintf("%s GiB", floatToString(gb, 2))
|
||||
}
|
||||
|
||||
func floatToString(f float64, p uint8) string {
|
||||
fs := fmt.Sprintf("%#f%0*s", f, p, "")
|
||||
idx := strings.IndexRune(fs, '.')
|
||||
return fs[:idx+int(p)+1]
|
||||
}
|
||||
296
pkg/cmd/cache/list/list_test.go
vendored
Normal file
296
pkg/cmd/cache/list/list_test.go
vendored
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wants ListOptions
|
||||
wantsErr string
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
wants: ListOptions{
|
||||
Limit: 30,
|
||||
Order: "desc",
|
||||
Sort: "last_accessed_at",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with limit",
|
||||
input: "--limit 100",
|
||||
wants: ListOptions{
|
||||
Limit: 100,
|
||||
Order: "desc",
|
||||
Sort: "last_accessed_at",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid limit",
|
||||
input: "-L 0",
|
||||
wantsErr: "invalid limit: 0",
|
||||
},
|
||||
{
|
||||
name: "with sort",
|
||||
input: "--sort created_at",
|
||||
wants: ListOptions{
|
||||
Limit: 30,
|
||||
Order: "desc",
|
||||
Sort: "created_at",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with order",
|
||||
input: "--order asc",
|
||||
wants: ListOptions{
|
||||
Limit: 30,
|
||||
Order: "asc",
|
||||
Sort: "last_accessed_at",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
var gotOpts *ListOptions
|
||||
cmd := NewCmdList(f, func(opts *ListOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr != "" {
|
||||
assert.EqualError(t, err, tt.wantsErr)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wants.Limit, gotOpts.Limit)
|
||||
assert.Equal(t, tt.wants.Sort, gotOpts.Sort)
|
||||
assert.Equal(t, tt.wants.Order, gotOpts.Order)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRun(t *testing.T) {
|
||||
var now = time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
opts ListOptions
|
||||
stubs func(*httpmock.Registry)
|
||||
tty bool
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
wantStderr string
|
||||
wantStdout string
|
||||
}{
|
||||
{
|
||||
name: "displays results tty",
|
||||
tty: true,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{
|
||||
{
|
||||
Id: 1,
|
||||
Key: "foo",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 100,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Key: "bar",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 1024,
|
||||
},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantStdout: heredoc.Doc(`
|
||||
|
||||
Showing 2 of 2 caches in OWNER/REPO
|
||||
|
||||
ID KEY SIZE CREATED ACCESSED
|
||||
1 foo 100 B about 2 years ago about 1 year ago
|
||||
2 bar 1.00 KiB about 2 years ago about 1 year ago
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "displays results non-tty",
|
||||
tty: false,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{
|
||||
{
|
||||
Id: 1,
|
||||
Key: "foo",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 100,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Key: "bar",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 1024,
|
||||
},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantStdout: "1\tfoo\t100 B\t2021-01-01T01:01:01Z\t2022-01-01T01:01:01Z\n2\tbar\t1.00 KiB\t2021-01-01T01:01:01Z\t2022-01-01T01:01:01Z\n",
|
||||
},
|
||||
{
|
||||
name: "displays no results",
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{},
|
||||
TotalCount: 0,
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "No caches found in OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "displays list error",
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StatusStringResponse(404, "Not Found"),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "X Failed to get caches: HTTP 404 (https://api.github.com/repos/OWNER/REPO/actions/caches?per_page=100)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.stubs != nil {
|
||||
tt.stubs(reg)
|
||||
}
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
tt.opts.Now = now
|
||||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
defer reg.Verify(t)
|
||||
|
||||
err := listRun(&tt.opts)
|
||||
if tt.wantErr {
|
||||
if tt.wantErrMsg != "" {
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_humanFileSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int64
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "min bytes",
|
||||
size: 1,
|
||||
want: "1 B",
|
||||
},
|
||||
{
|
||||
name: "max bytes",
|
||||
size: 1023,
|
||||
want: "1023 B",
|
||||
},
|
||||
{
|
||||
name: "min kibibytes",
|
||||
size: 1024,
|
||||
want: "1.00 KiB",
|
||||
},
|
||||
{
|
||||
name: "max kibibytes",
|
||||
size: 1024*1024 - 1,
|
||||
want: "1023.99 KiB",
|
||||
},
|
||||
{
|
||||
name: "min mibibytes",
|
||||
size: 1024 * 1024,
|
||||
want: "1.00 MiB",
|
||||
},
|
||||
{
|
||||
name: "fractional mibibytes",
|
||||
size: 1024*1024*12 + 1024*350,
|
||||
want: "12.34 MiB",
|
||||
},
|
||||
{
|
||||
name: "max mibibytes",
|
||||
size: 1024*1024*1024 - 1,
|
||||
want: "1023.99 MiB",
|
||||
},
|
||||
{
|
||||
name: "min gibibytes",
|
||||
size: 1024 * 1024 * 1024,
|
||||
want: "1.00 GiB",
|
||||
},
|
||||
{
|
||||
name: "fractional gibibytes",
|
||||
size: 1024 * 1024 * 1024 * 1.5,
|
||||
want: "1.50 GiB",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := humanFileSize(tt.size); got != tt.want {
|
||||
t.Errorf("humanFileSize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
73
pkg/cmd/cache/shared/shared.go
vendored
Normal file
73
pkg/cmd/cache/shared/shared.go
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Id int `json:"id"`
|
||||
Key string `json:"key"`
|
||||
LastAccessedAt time.Time `json:"last_accessed_at"`
|
||||
Ref string `json:"ref"`
|
||||
SizeInBytes int64 `json:"size_in_bytes"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type CachePayload struct {
|
||||
ActionsCaches []Cache `json:"actions_caches"`
|
||||
TotalCount int `json:"total_count"`
|
||||
}
|
||||
|
||||
type GetCachesOptions struct {
|
||||
Limit int
|
||||
Order string
|
||||
Sort string
|
||||
}
|
||||
|
||||
// Return a list of caches for a repository. Pass a negative limit to request
|
||||
// all pages from the API until all caches have been fetched.
|
||||
func GetCaches(client *api.Client, repo ghrepo.Interface, opts GetCachesOptions) (*CachePayload, error) {
|
||||
path := fmt.Sprintf("repos/%s/actions/caches", ghrepo.FullName(repo))
|
||||
|
||||
perPage := 100
|
||||
if opts.Limit > 0 && opts.Limit < 100 {
|
||||
perPage = opts.Limit
|
||||
}
|
||||
path += fmt.Sprintf("?per_page=%d", perPage)
|
||||
|
||||
if opts.Sort != "" {
|
||||
path += fmt.Sprintf("&sort=%s", opts.Sort)
|
||||
}
|
||||
if opts.Order != "" {
|
||||
path += fmt.Sprintf("&direction=%s", opts.Order)
|
||||
}
|
||||
|
||||
var result *CachePayload
|
||||
pagination:
|
||||
for path != "" {
|
||||
var response CachePayload
|
||||
var err error
|
||||
path, err = client.RESTWithNext(repo.RepoHost(), "GET", path, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = &response
|
||||
} else {
|
||||
result.ActionsCaches = append(result.ActionsCaches, response.ActionsCaches...)
|
||||
}
|
||||
|
||||
if opts.Limit > 0 && len(result.ActionsCaches) >= opts.Limit {
|
||||
result.ActionsCaches = result.ActionsCaches[:opts.Limit]
|
||||
break pagination
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
68
pkg/cmd/cache/shared/shared_test.go
vendored
Normal file
68
pkg/cmd/cache/shared/shared_test.go
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCaches(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts GetCachesOptions
|
||||
stubs func(*httpmock.Registry)
|
||||
wantsCount int
|
||||
}{
|
||||
{
|
||||
name: "no caches",
|
||||
opts: GetCachesOptions{},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StringResponse(`{"actions_caches": [], "total_count": 0}`),
|
||||
)
|
||||
},
|
||||
wantsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "limits cache count",
|
||||
opts: GetCachesOptions{Limit: 1},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StringResponse(`{"actions_caches": [{"id": 1}, {"id": 2}], "total_count": 2}`),
|
||||
)
|
||||
},
|
||||
wantsCount: 1,
|
||||
},
|
||||
{
|
||||
name: "negative limit returns all caches",
|
||||
opts: GetCachesOptions{Limit: -1},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StringResponse(`{"actions_caches": [{"id": 1}, {"id": 2}], "total_count": 2}`),
|
||||
)
|
||||
},
|
||||
wantsCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
tt.stubs(reg)
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
repo, err := ghrepo.FromFullName("OWNER/REPO")
|
||||
assert.NoError(t, err)
|
||||
result, err := GetCaches(client, repo, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantsCount, len(result.ActionsCaches))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import (
|
|||
apiCmd "github.com/cli/cli/v2/pkg/cmd/api"
|
||||
authCmd "github.com/cli/cli/v2/pkg/cmd/auth"
|
||||
browseCmd "github.com/cli/cli/v2/pkg/cmd/browse"
|
||||
cacheCmd "github.com/cli/cli/v2/pkg/cmd/cache"
|
||||
codespaceCmd "github.com/cli/cli/v2/pkg/cmd/codespace"
|
||||
completionCmd "github.com/cli/cli/v2/pkg/cmd/completion"
|
||||
configCmd "github.com/cli/cli/v2/pkg/cmd/config"
|
||||
|
|
@ -123,6 +124,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
cmd.AddCommand(runCmd.NewCmdRun(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(workflowCmd.NewCmdWorkflow(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(labelCmd.NewCmdLabel(&repoResolvingCmdFactory))
|
||||
cmd.AddCommand(cacheCmd.NewCmdCache(&repoResolvingCmdFactory))
|
||||
|
||||
// Help topics
|
||||
cmd.AddCommand(NewHelpTopic(f.IOStreams, "environment"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue