diff --git a/client/client.go b/client/client.go index 6dff196..44b4c1f 100644 --- a/client/client.go +++ b/client/client.go @@ -16,12 +16,18 @@ import ( "github.com/cszatmary/shed/lockfile" "github.com/cszatmary/shed/tool" "github.com/sirupsen/logrus" + "golang.org/x/mod/semver" ) const LockfileName = "shed.lock" -// noneVersion is a special module version that signifies the module should be removed. -const noneVersion = "none" +const ( + // noneVersion is a special module version that signifies the module should be removed. + noneVersion = "none" + // latestVersion is a special module version that signifies the latest + // available version should be installed. + latestVersion = "latest" +) // ResolveLockfilePath resolves the path to the nearest shed lockfile starting at dir. // It will keep searching parent directories until either a lockfile is found, @@ -146,8 +152,19 @@ func (s *Shed) writeLockfile(op errors.Op) error { return nil } -// Get computes a set of tools that should be installed. It can be given zero or -// more tools as arguments. These will be unioned with the tools in the lockfile +// GetOptions is used to configure Shed.Get. +type GetOptions struct { + // ToolNames is a list of tools that should be installed. + // These will be unioned with the tools specified in the lockfile. + ToolNames []string + // Update sets whether or not tools should be updated to the latest available + // minor or patch version. If ToolNames is not empty, only those tools will be + // updated. Otherwise, all tools in the lockfile will be updated. + Update bool +} + +// Get computes a set of tools that should be installed. Zero or more tools can be +// specified in opts. These will be unioned with the tools in the lockfile // to produce a final set of tools to install. Get will return an InstallSet instance // which can be used to perform the actual installation. // @@ -156,7 +173,9 @@ func (s *Shed) writeLockfile(op errors.Op) error { // // All tool names provided must be full import paths, not binary names. // If a tool name is invalid, Get will return an error. -func (s *Shed) Get(toolNames ...string) (*InstallSet, error) { +// +// If opts.Update is set, tool names must not include version suffixes. +func (s *Shed) Get(opts GetOptions) (*InstallSet, error) { const op = errors.Op("Shed.Get") // Collect all the tools that need to be installed. // Merge the given tools with what exists in the lockfile. @@ -164,7 +183,7 @@ func (s *Shed) Get(toolNames ...string) (*InstallSet, error) { var tools []tool.Tool var errs errors.List - for _, toolName := range toolNames { + for _, toolName := range opts.ToolNames { // This also serves to validate the the given tool name is a valid module name // Use ParseLax since the version might be a query that should be passed to go get. t, err := tool.ParseLax(toolName) @@ -172,6 +191,15 @@ func (s *Shed) Get(toolNames ...string) (*InstallSet, error) { errs = append(errs, errors.New(fmt.Sprintf("invalid tool name %s", toolName), op, err)) continue } + if opts.Update { + // Version is not allowed if updating, since the latest version will be installed. + if t.Version != "" && t.Version != noneVersion && t.Version != latestVersion { + msg := fmt.Sprintf("tool %s must not have a version when updating", t) + errs = append(errs, errors.New(errors.Invalid, msg, op)) + continue + } + t.Version = latestVersion + } seenTools[t.ImportPath] = true tools = append(tools, t) } @@ -179,13 +207,21 @@ func (s *Shed) Get(toolNames ...string) (*InstallSet, error) { return nil, errs } + // If update and no tools provided update all in the lockfile. + updateAll := opts.Update && len(opts.ToolNames) == 0 // Take union with lockfile it := s.lf.Iter() for it.Next() { t := it.Value() - if ok := seenTools[t.ImportPath]; !ok { - tools = append(tools, t) + if ok := seenTools[t.ImportPath]; ok { + continue } + // Skip tools with a prelease version installed since the latest version might + // actually be older than the current version which was explicitly installed. + if updateAll && semver.Prerelease(t.Version) == "" { + t.Version = latestVersion + } + tools = append(tools, t) } return &InstallSet{s: s, tools: tools}, nil } @@ -283,7 +319,7 @@ func (is *InstallSet) Apply(ctx context.Context) error { for _, t := range completedTools { if t.Version == noneVersion { // Uninstall the tool by removing it from the lockfile. - // Unlike Uninstall() this will not error if the tool is not in the lockfile, + // This will not error if the tool is not in the lockfile, // instead it will be silently ignored. t.Version = "" is.s.lf.DeleteTool(t) diff --git a/client/client_test.go b/client/client_test.go index af76827..4035de8 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -110,6 +110,7 @@ var availableTools = map[string]map[string]string{ "github.com/cszatmary/go-fish": { "v0.1.0": "v0.1.0", "22d10c9b658df297b17b33c836a60fb943ef5a5f": "v0.0.0-20201203230243-22d10c9b658d", + "v0.0.0-20201203230243-22d10c9b658d": "v0.0.0-20201203230243-22d10c9b658d", }, "github.com/golangci/golangci-lint/cmd/golangci-lint": { "v1.33.0": "v1.33.0", @@ -165,6 +166,7 @@ func TestGet(t *testing.T) { name string lockfileTools []tool.Tool installTools []string + update bool wantLen int wantTools []tool.Tool }{ @@ -247,6 +249,55 @@ func TestGet(t *testing.T) { {ImportPath: "github.com/Shopify/ejson/cmd/ejson", Version: "v1.1.0"}, }, }, + { + name: "update all in lockfile", + lockfileTools: []tool.Tool{ + {ImportPath: "github.com/cszatmary/go-fish", Version: "v0.1.0"}, + {ImportPath: "github.com/golangci/golangci-lint/cmd/golangci-lint", Version: "v1.28.3"}, + {ImportPath: "github.com/Shopify/ejson/cmd/ejson", Version: "v1.1.0"}, + }, + installTools: nil, + update: true, + wantLen: 3, + wantTools: []tool.Tool{ + {ImportPath: "github.com/cszatmary/go-fish", Version: "v0.1.0"}, + {ImportPath: "github.com/golangci/golangci-lint/cmd/golangci-lint", Version: "v1.33.0"}, + {ImportPath: "github.com/Shopify/ejson/cmd/ejson", Version: "v1.2.2"}, + }, + }, + { + name: "update specific tools", + lockfileTools: []tool.Tool{ + {ImportPath: "github.com/cszatmary/go-fish", Version: "v0.1.0"}, + {ImportPath: "github.com/golangci/golangci-lint/cmd/golangci-lint", Version: "v1.28.3"}, + {ImportPath: "github.com/Shopify/ejson/cmd/ejson", Version: "v1.1.0"}, + }, + installTools: []string{ + "github.com/Shopify/ejson/cmd/ejson", + }, + update: true, + wantLen: 3, + wantTools: []tool.Tool{ + {ImportPath: "github.com/cszatmary/go-fish", Version: "v0.1.0"}, + {ImportPath: "github.com/golangci/golangci-lint/cmd/golangci-lint", Version: "v1.28.3"}, + {ImportPath: "github.com/Shopify/ejson/cmd/ejson", Version: "v1.2.2"}, + }, + }, + { + name: "does not update prerelease versions", + lockfileTools: []tool.Tool{ + {ImportPath: "github.com/cszatmary/go-fish", Version: "v0.0.0-20201203230243-22d10c9b658d"}, + {ImportPath: "github.com/golangci/golangci-lint/cmd/golangci-lint", Version: "v1.28.3"}, + {ImportPath: "github.com/Shopify/ejson/cmd/ejson", Version: "v1.1.0"}, + }, + update: true, + wantLen: 3, + wantTools: []tool.Tool{ + {ImportPath: "github.com/cszatmary/go-fish", Version: "v0.0.0-20201203230243-22d10c9b658d"}, + {ImportPath: "github.com/golangci/golangci-lint/cmd/golangci-lint", Version: "v1.33.0"}, + {ImportPath: "github.com/Shopify/ejson/cmd/ejson", Version: "v1.2.2"}, + }, + }, } for _, tt := range tests { @@ -271,7 +322,10 @@ func TestGet(t *testing.T) { t.Fatalf("failed to create shed client %v", err) } - installSet, err := s.Get(tt.installTools...) + installSet, err := s.Get(client.GetOptions{ + ToolNames: tt.installTools, + Update: tt.update, + }) if err != nil { t.Errorf("want nil error, got %v", err) } @@ -332,11 +386,13 @@ func TestGetError(t *testing.T) { t.Fatalf("failed to create shed client %v", err) } - _, err = s.Get( - "github.com/cszatmary/go-fish", - "golangci-lint", - "github.com/Shopify/ejson/cmd/ejson@v1.2.2", - ) + _, err = s.Get(client.GetOptions{ + ToolNames: []string{ + "github.com/cszatmary/go-fish", + "golangci-lint", + "github.com/Shopify/ejson/cmd/ejson@v1.2.2", + }, + }) errList, ok := err.(errors.List) if !ok { t.Errorf("want error to be errors.List, got %s: %T", err, err) @@ -417,7 +473,7 @@ func TestList(t *testing.T) { // Install tools, otherwise List might error ctx := context.Background() - installSet, err := s.Get() + installSet, err := s.Get(client.GetOptions{}) if err != nil { t.Fatalf("failed to install tools %v", err) } diff --git a/cmd/get.go b/cmd/get.go index 9ed13ad..0c21172 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/cszatmary/shed/client" "github.com/cszatmary/shed/internal/spinner" "github.com/cszatmary/shed/tool" "github.com/spf13/cobra" @@ -10,6 +11,7 @@ import ( func newGetCommand(c *container) *cobra.Command { var getOpts struct { + update bool concurrency int } @@ -30,6 +32,10 @@ Tools can be uninstalled by using the special '@none' version suffix. If no tools are provided, then shed will simply install all tools in the lockfile. +The '-u, --update' flag instructs get to update the provided tools to use newer minor or patch releases when available. +If no tools are provided, all tools in the lockfile will be updated. When this flag is used, tools are not allowed +to have a version suffix. + Examples: Install the latest version of a tool: @@ -46,7 +52,15 @@ Install all tools specified in shed.lock: Uninstall a tool: - shed get golang.org/x/tools/cmd/stringer@none`, + shed get golang.org/x/tools/cmd/stringer@none + +Update a specific tool to the latest minor or patch version: + + shed get -u golang.org/x/tools/cmd/stringer + +Update all tools in the lockfile to their latest minor or patch version: + + shed get -u`, RunE: func(cmd *cobra.Command, args []string) error { if getOpts.concurrency < 0 { return &exitError{ @@ -56,7 +70,10 @@ Uninstall a tool: } } - installSet, err := c.shed.Get(args...) + installSet, err := c.shed.Get(client.GetOptions{ + ToolNames: args, + Update: getOpts.update, + }) if err != nil { return fmt.Errorf("unable to determine list of tools to install: %w", err) } @@ -95,6 +112,7 @@ Uninstall a tool: }, } + getCmd.Flags().BoolVarP(&getOpts.update, "update", "u", false, "update tools to their latest minor or patch version") getCmd.Flags().IntVarP(&getOpts.concurrency, "concurrency", "c", 0, "amount of tasks to run concurrently (default: number of CPUs)") return getCmd } diff --git a/cmd/list.go b/cmd/list.go index 3219b1b..4d148a6 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -19,7 +19,7 @@ func newListCommand(c *container) *cobra.Command { Short: "List Go tools specified in shed.lock.", Long: `shed list prints a list of tools specified in shed.lock. Each tool will consist of the import path and the version. -The --upgrades or -u flag causes shed to list information about available upgrades for each tool. +The '-u, --updates' flag causes shed to list information about available upgrades for each tool. If a newer version is found for a tool, shed will print it in brackets after the current version. For example, 'shed list -u' might print: diff --git a/cmd/root.go b/cmd/root.go index 843d841..85078dc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -213,7 +213,7 @@ func newRootCommand(c *container) *cobra.Command { newRunCommand(c), ) - rootCmd.PersistentFlags().BoolVar(&c.opts.verbose, "verbose", false, "enable verbose logging") + rootCmd.PersistentFlags().BoolVarP(&c.opts.verbose, "verbose", "v", false, "enable verbose logging") rootCmd.PersistentFlags().StringVar(&c.opts.progressMode, "progress", "auto", "sets if a progress spinner should be used, valid values: on, off, auto") return rootCmd } diff --git a/shed.lock b/shed.lock index 117c7b6..9645164 100644 --- a/shed.lock +++ b/shed.lock @@ -1,19 +1,19 @@ { "tools": { "github.com/cszatmary/go-fish": { - "version": "v0.0.0-20210414180724-443d9e00794d" + "version": "v0.1.0" }, "github.com/golangci/golangci-lint/cmd/golangci-lint": { - "version": "v1.39.0" + "version": "v1.41.1" }, "github.com/goreleaser/godownloader": { "version": "v0.1.1-0.20200813202458-888d721de8bd" }, "github.com/goreleaser/goreleaser": { - "version": "v0.162.0" + "version": "v0.174.2" }, "golang.org/x/tools/cmd/goimports": { - "version": "v0.1.0" + "version": "v0.1.5" } } } \ No newline at end of file