Skip to content

Commit

Permalink
feat: Add --update flag to get to update tools to the latest version (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
cszatmary committed Aug 8, 2021
1 parent 73900de commit c86c2f5
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 24 deletions.
54 changes: 45 additions & 9 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
//
Expand All @@ -156,36 +173,55 @@ 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.
seenTools := make(map[string]bool)
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)
if err != nil {
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)
}
if len(errs) > 0 {
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
}
Expand Down Expand Up @@ -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)
Expand Down
70 changes: 63 additions & 7 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -165,6 +166,7 @@ func TestGet(t *testing.T) {
name string
lockfileTools []tool.Tool
installTools []string
update bool
wantLen int
wantTools []tool.Tool
}{
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
22 changes: 20 additions & 2 deletions cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package cmd
import (
"fmt"

"github.com/cszatmary/shed/client"
"github.com/cszatmary/shed/internal/spinner"
"github.com/cszatmary/shed/tool"
"github.com/spf13/cobra"
)

func newGetCommand(c *container) *cobra.Command {
var getOpts struct {
update bool
concurrency int
}

Expand All @@ -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:
Expand All @@ -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{
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
8 changes: 4 additions & 4 deletions shed.lock
Original file line number Diff line number Diff line change
@@ -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"
}
}
}

0 comments on commit c86c2f5

Please sign in to comment.