diff --git a/README.md b/README.md index d5aa903..98dbf22 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ cachew namespaces # Directory snapshots cachew save [paths...] (--key | -H ) [--ttl 1h] [--exclude pattern] -cachew restore (--key | -H ) +cachew restore (--key | -H ) # exit 0 hit, 2 miss, 1 error # Git cachew git restore [--no-bundle] diff --git a/cmd/cachew/main.go b/cmd/cachew/main.go index db9430f..3deee38 100644 --- a/cmd/cachew/main.go +++ b/cmd/cachew/main.go @@ -38,7 +38,9 @@ type CLI struct { Git GitCmd `cmd:"" help:"Git-aware operations." group:"Git:"` } -func main() { +func main() { os.Exit(run()) } + +func run() int { cli := CLI{} kctx := kong.Parse(&cli, kong.UsageOnError(), kong.HelpOptions{Compact: true}, kong.DefaultEnvars("CACHEW"), kong.Bind(&cli)) ctx := context.Background() @@ -56,9 +58,19 @@ func main() { kctx.BindTo(ctx, (*context.Context)(nil)) kctx.Bind(c) kctx.Bind(c.HTTP()) - kctx.FatalIfErrorf(kctx.Run(ctx)) + err := kctx.Run(ctx) + if errors.Is(err, errCacheMiss) { + return 2 + } + kctx.FatalIfErrorf(err) + return 0 } +// errCacheMiss signals that a restore found no object for the requested key. +// Sentinel so main can exit with code 2 (distinct from generic errors at +// code 1), matching conventions used by grep/diff. +var errCacheMiss = errors.New("cache miss") + type GetCmd struct { Namespace client.Namespace `arg:"" help:"Namespace for organizing cache objects."` Key PlatformKey `arg:"" help:"Object key (hex or string)."` @@ -222,7 +234,8 @@ func (c *RestoreCmd) Run(ctx context.Context, api *client.Client, cli *CLI) erro return errors.Wrap(err, "failed to restore") } if !hit { - return errors.Errorf("cache miss: %s", display) + fmt.Fprintf(os.Stderr, "Cache miss: %s\n", display) //nolint:forbidigo + return errCacheMiss } fmt.Fprintf(os.Stderr, "Restored: %s\n", display) //nolint:forbidigo diff --git a/cmd/cachew/save_test.go b/cmd/cachew/save_test.go index b0527ab..77c5edc 100644 --- a/cmd/cachew/save_test.go +++ b/cmd/cachew/save_test.go @@ -1,6 +1,9 @@ package main import ( + "context" + "net/http" + "net/http/httptest" "os" "path/filepath" "runtime" @@ -8,6 +11,7 @@ import ( "time" "github.com/alecthomas/assert/v2" + "github.com/alecthomas/errors" "github.com/block/cachew/client" ) @@ -53,3 +57,21 @@ func TestResolveKeyHashFilesNoMatch(t *testing.T) { _, _, err := resolveKey(&CLI{}, "", []string{filepath.Join(t.TempDir(), "missing-*")}) assert.Error(t, err) } + +func TestRestoreCacheMiss(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.NotFound(w, nil) //nolint:staticcheck + })) + defer srv.Close() + + api := client.New(srv.URL, nil) + defer api.Close() + + cmd := &RestoreCmd{ + Namespace: "test", + Directory: t.TempDir(), + Key: "absent", + } + err := cmd.Run(context.Background(), api, &CLI{}) + assert.True(t, errors.Is(err, errCacheMiss), "expected errCacheMiss sentinel, got %v", err) +}