From 10ac1c3e6335352c7d5e9c778179d6f786713687 Mon Sep 17 00:00:00 2001 From: Vasileios Pallas Date: Sun, 29 Oct 2023 02:03:03 +0200 Subject: [PATCH] feat: support multiple flags together (#9) * feat: support multiple flags together * fix: pr template --- .github/pull_request_template.md | 1 - README.md | 18 ++++- cli/cli.go | 66 +++++++++++++++ cmd/gvs/main.go | 103 ++++++------------------ files/files.go | 17 ++++ files/files_test.go | 74 +++++++++++++++++ flags/flags.go | 34 ++++++-- internal/testutils/fake_file_helpers.go | 4 + 8 files changed, 230 insertions(+), 87 deletions(-) create mode 100644 cli/cli.go diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 072a685..318b668 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,7 +14,6 @@ Fixes https://github.com/VassilisPallas/gvs/issues Tests -- [ ] I've added integration tests - [ ] I've added unit tests Documentation diff --git a/README.md b/README.md index d97c799..cfa76d0 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Installing version... ### Install specific version -To install a specific version without using the dropdown, use the `--install-version=value`. +To install a specific version without using the dropdown, use the `--install-version=value` flag. ```sh $ gvs --install-version=1.21.3 @@ -190,10 +190,24 @@ Installing version... Every time you install a new version, gvs keeps the previous installed versions, so you can easily change between them. If you want to delete all the unused versions and keep only the current one, use the `--delete-unused` flag. -In the below example, the versions `1.20` and `1.19` are previously installed, and since they are not used (neither of them is the current version you use), they will be deleted. +In the below example, the versions `1.20` and `1.19` are previously installed, and since they are not used (neither of them is the current version you use), they will be deleted after installing the new version 1.21.2. ```sh $ gvs --delete-unused +Use the arrow keys to navigate: ↓ ↑ → ← +? Select go version: + 1.21.3 + ▸ 1.21.2 + 1.21.1 + 1.21.0 + 1.20.10 + +✔ 1.21.2 +Downloading... +Compare Checksums... +Unzipping... +Installing version... +1.21.2 version is installed! Deleting go1.20. go1.20 is deleted. Deleting go1.19. diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..3bcd2a9 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,66 @@ +package cli + +import ( + "errors" + "fmt" + "runtime" + + "github.com/VassilisPallas/gvs/logger" + "github.com/VassilisPallas/gvs/version" +) + +type CLI struct { + versions []*version.ExtendedVersion + versioner version.Versioner + log logger.Logger +} + +func (cli CLI) Install(selectedVersion *version.ExtendedVersion) error { + cli.log.Info("selected %s version\n", selectedVersion.Version) + return cli.versioner.Install(selectedVersion, runtime.GOOS, runtime.GOARCH) +} + +func (cli CLI) InstallVersion(goVersion string) error { + semver := &version.Semver{} + err := version.ParseSemver(goVersion, semver) + if err != nil { + return err + } + + selectedVersion := cli.versioner.FindVersionBasedOnSemverName(cli.versions, semver) + if selectedVersion != nil { + return fmt.Errorf("%s is not a valid version", semver.GetVersion()) + } + + return cli.Install(selectedVersion) +} + +func (cli CLI) InstallLatestVersion() error { + selectedIndex := cli.versioner.GetLatestVersion(cli.versions) + if selectedIndex == -1 { + return errors.New("latest version not found") + } + + selectedVersion := cli.versions[selectedIndex] + + return cli.Install(selectedVersion) +} + +func (cli CLI) DeleteUnusedVersions() error { + deleted_count, err := cli.versioner.DeleteUnusedVersions(cli.versions) + if err != nil { + return err + } + + if deleted_count > 0 { + cli.log.PrintMessage("All the unused version are deleted!") + } else { + cli.log.PrintMessage("Nothing to delete") + } + + return nil +} + +func New(versions []*version.ExtendedVersion, versioner version.Versioner, log logger.Logger) CLI { + return CLI{versions: versions, versioner: versioner, log: log} +} diff --git a/cmd/gvs/main.go b/cmd/gvs/main.go index 3e65e17..97b8aaa 100644 --- a/cmd/gvs/main.go +++ b/cmd/gvs/main.go @@ -3,10 +3,10 @@ package main import ( "net/http" "os" - "runtime" "time" "github.com/VassilisPallas/gvs/api_client" + "github.com/VassilisPallas/gvs/cli" "github.com/VassilisPallas/gvs/clock" cf "github.com/VassilisPallas/gvs/config" "github.com/VassilisPallas/gvs/files" @@ -16,7 +16,6 @@ import ( "github.com/VassilisPallas/gvs/pkg/unzip" "github.com/VassilisPallas/gvs/version" "github.com/manifoldco/promptui" - "golang.org/x/mod/modfile" ) var ( @@ -30,12 +29,12 @@ var ( func parseFlags() { set := flags.FlagSet{} - set.FlagBool(&showAllVersions, "show-all", false, "Show both stable and unstable versions.") - set.FlagBool(&installLatest, "install-latest", false, "Install latest stable version.") - set.FlagBool(&deleteUnused, "delete-unused", false, "Delete all unused versions that were installed before.") - set.FlagBool(&refreshVersions, "refresh-versions", false, "Fetch again go versions in case the cached ones are stale.") - set.FlagStr(&specificVersion, "install-version", "", "Pass the version you want to install instead of selecting from the dropdown. If you do not specify the minor or the patch version, the latest one will be selected.") - set.FlagBool(&fromModFile, "from-mod", false, "Install the version that will be found on the go.mod file. The go.mod file should be on the same path you run gvs. If the version in the go.mod file do not specify the minor or the patch version, the latest one will be selected.") + set.FlagBool(&showAllVersions, "show-all", 'a', false, "Show both stable and unstable versions.") + set.FlagBool(&installLatest, "install-latest", 'l', false, "Install latest stable version.") + set.FlagBool(&deleteUnused, "delete-unused", 'd', false, "Delete all unused versions that were installed before.") + set.FlagBool(&refreshVersions, "refresh-versions", 'r', false, "Fetch again go versions in case the cached ones are stale.") + set.FlagStr(&specificVersion, "install-version", 'v', "", "Pass the version you want to install instead of selecting from the dropdown. If you do not specify the minor or the patch version, the latest one will be selected.") + set.FlagBool(&fromModFile, "from-mod", 'm', false, "Install the version that will be found on the go.mod file. The go.mod file should be on the same path you run gvs. If the version in the go.mod file do not specify the minor or the patch version, the latest one will be selected.") set.Parse() } @@ -79,40 +78,20 @@ func main() { return } + cli := cli.New(versions, versioner, log) + switch { case fromModFile: log.Info("install version from go.mod file option selected") - buf, err := fs.ReadFile("./go.mod") - if err != nil { - log.PrintError(err.Error()) - os.Exit(1) - return - } - - f, err := modfile.Parse("go.mod", buf, nil) + version, err := fileHelpers.ReadVersionFromMod() if err != nil { log.PrintError(err.Error()) os.Exit(1) return } - semver := &version.Semver{} - err = version.ParseSemver(f.Go.Version, semver) - if err != nil { - log.PrintError(err.Error()) - os.Exit(1) - return - } - - selectedVersion := versioner.FindVersionBasedOnSemverName(versions, semver) - if selectedVersion == nil { - log.PrintError("%s is not a valid version.", semver.GetVersion()) - os.Exit(1) - return - } - - err = versioner.Install(selectedVersion, runtime.GOOS, runtime.GOARCH) + err = cli.InstallVersion(version) if err != nil { log.PrintError(err.Error()) os.Exit(1) @@ -121,56 +100,16 @@ func main() { case specificVersion != "": log.Info("install specific version option selected") - semver := &version.Semver{} - err := version.ParseSemver(specificVersion, semver) + err := cli.InstallVersion(specificVersion) if err != nil { log.PrintError(err.Error()) os.Exit(1) return } - - selectedVersion := versioner.FindVersionBasedOnSemverName(versions, semver) - if selectedVersion == nil { - log.PrintError("%s is not a valid version.", semver.GetVersion()) - os.Exit(1) - return - } - - err = versioner.Install(selectedVersion, runtime.GOOS, runtime.GOARCH) - if err != nil { - log.PrintError(err.Error()) - os.Exit(1) - return - } - case deleteUnused: - log.Info("deleteUnused option selected") - - deleted_count, err := versioner.DeleteUnusedVersions(versions) - if err != nil { - log.PrintError(err.Error()) - os.Exit(1) - return - } - - if deleted_count > 0 { - log.PrintMessage("All the unused version are deleted!") - } else { - log.PrintMessage("Nothing to delete") - } case installLatest: log.Info("install latest option selected") - selectedIndex := versioner.GetLatestVersion(versions) - if selectedIndex == -1 { - log.PrintError("latest version not found") - os.Exit(1) - return - } - - selectedVersion := versions[selectedIndex] - - log.Info("selected %s version", selectedVersion.Version) - err := versioner.Install(selectedVersion, runtime.GOOS, runtime.GOARCH) + err := cli.InstallLatestVersion() if err != nil { log.PrintError(err.Error()) os.Exit(1) @@ -200,10 +139,20 @@ func main() { return } - selectedVersion := promptVersions[selectedIndex] + err = cli.Install(promptVersions[selectedIndex]) + if err != nil { + log.PrintError(err.Error()) + os.Exit(1) + return + } + } + + // execute this command at the end, after any other selected flag + // to delete the unused version if needed. + if deleteUnused { + log.Info("deleteUnused option selected") - log.Info("selected %s version\n", selectedVersion.Version) - err := versioner.Install(selectedVersion, runtime.GOOS, runtime.GOARCH) + err := cli.DeleteUnusedVersions() if err != nil { log.PrintError(err.Error()) os.Exit(1) diff --git a/files/files.go b/files/files.go index a729710..0a436ff 100644 --- a/files/files.go +++ b/files/files.go @@ -14,6 +14,7 @@ import ( "github.com/VassilisPallas/gvs/clock" "github.com/VassilisPallas/gvs/logger" "github.com/VassilisPallas/gvs/pkg/unzip" + "golang.org/x/mod/modfile" ) // FileHelpers is the interface that wraps the basic methods for working with files. @@ -79,6 +80,8 @@ type FileHelpers interface { // GetLatestCreatedGoVersionDirectory must return the name of the latest modified directory and // a non-null error if the operation is successful. GetLatestCreatedGoVersionDirectory() (string, error) + + ReadVersionFromMod() (string, error) } // Helper is the struct that implements the FileHelpers interface @@ -371,6 +374,20 @@ func (h Helper) GetLatestCreatedGoVersionDirectory() (string, error) { return dirName, nil } +func (h Helper) ReadVersionFromMod() (string, error) { + buf, err := h.fileSystem.ReadFile("./go.mod") + if err != nil { + return "", err + } + + f, err := modfile.Parse("go.mod", buf, nil) + if err != nil { + return "", err + } + + return f.Go.Version, nil +} + // New returns a *Helper instance that implements the FileHelpers interface. // Each call to New returns a distinct *Helper instance even if the parameters are identical. func New(fs FS, clock clock.Clock, unzipper unzip.Unzipper, log *logger.Log) *Helper { diff --git a/files/files_test.go b/files/files_test.go index d06606d..ec98671 100644 --- a/files/files_test.go +++ b/files/files_test.go @@ -972,3 +972,77 @@ func TestGetLatestCreatedGoVersionDirectory(t *testing.T) { }) } } + +func TestReadVersionFromMod(t *testing.T) { + var file_bytes = []byte(` + module module_name + + go 1.20 + + require ( + foo/bar v1.2.3 + ) + `) + + testCases := []struct { + testTitle string + modFileBytes []byte + modeFileError error + expectedError error + expectedVersion string + }{ + { + testTitle: "should return an error when reading the go.mod file returns an error", + modFileBytes: nil, + modeFileError: errors.New("some error while reading th go.mod file"), + expectedError: errors.New("some error while reading th go.mod file"), + expectedVersion: "", + }, + { + testTitle: "should return an error when parsing the go.mod file returns an error", + modFileBytes: []byte(`wrong`), + modeFileError: nil, + expectedError: errors.New("go.mod:1: unknown directive: wrong"), + expectedVersion: "", + }, + { + testTitle: "should return the version from go.mod", + modFileBytes: file_bytes, + modeFileError: nil, + expectedError: nil, + expectedVersion: "1.20", + }, + } + + for _, tc := range testCases { + t.Run(tc.testTitle, func(t *testing.T) { + fs := testutils.FakeFileSystem{ + HomeDir: "/tmp", + ReadFileBytes: tc.modFileBytes, + ReadFileError: tc.modeFileError, + } + + clock := testutils.FakeClock{ + UseRealIsBefore: true, + UseRealIsAfter: true, + } + fileHelper := createFileHelper(&testutils.FakeStdout{}, nil, fs, testutils.FakeUnzipper{}, clock) + goVersion, err := fileHelper.ReadVersionFromMod() + + if tc.expectedError == nil && err != nil { + t.Errorf("error should be nil, instead got %q", err.Error()) + return + } + + if tc.expectedError != nil && err.Error() != tc.expectedError.Error() { + t.Errorf("error should be %q, instead got %q", tc.expectedError.Error(), err.Error()) + return + } + + if goVersion != tc.expectedVersion { + t.Errorf("the version should be %q, instead got %q", tc.expectedVersion, goVersion) + return + } + }) + } +} diff --git a/flags/flags.go b/flags/flags.go index 5124aec..1b576c4 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -9,9 +9,12 @@ import ( // Flag contains the information for the flag. type Flag struct { - // The name of the flag + // The name of the flag. name string + // a short name for the flag. + shortName string + // A boolean that represents if the flag expects a value to be passed. // // e.g. --install-version 21.3 @@ -25,6 +28,15 @@ type Flag struct { // If the flag expects a value, the result will be like: --some-flag=value func (f Flag) getHelpName() string { flagName := fmt.Sprintf("--%s", f.name) + + if f.acceptsVale { + flagName += "=value" + } + + if f.shortName != "" { + flagName += fmt.Sprintf(", -%s", f.shortName) + } + if f.acceptsVale { flagName += "=value" } @@ -39,20 +51,28 @@ type FlagSet struct { flags []Flag } -// FlagBool defines a bool flag with specified name, default value, and usage string. +// FlagBool defines a bool flag with specified name, short name (single character), default value, and usage string. // The argument p points to a bool variable in which to store the value of the flag. // FlagBool also appends the flag to the FlagSet array. -func (s *FlagSet) FlagBool(p *bool, name string, value bool, usage string) { - s.flags = append(s.flags, Flag{name: name, acceptsVale: false}) +func (s *FlagSet) FlagBool(p *bool, name string, shortName rune, value bool, usage string) { + s.flags = append(s.flags, Flag{name: name, shortName: string(shortName), acceptsVale: false}) flag.BoolVar(p, name, value, usage) + + if string(shortName) != "" { + flag.BoolVar(p, string(shortName), value, usage) + } } -// StringVar defines a string flag with specified name, default value, and usage string. +// StringVar defines a string flag with specified name, short name (single character), default value, and usage string. // The argument p points to a string variable in which to store the value of the flag. // FlagBool also appends the flag to the FlagSet array. -func (s *FlagSet) FlagStr(p *string, name string, value string, usage string) { - s.flags = append(s.flags, Flag{name: name, acceptsVale: true}) +func (s *FlagSet) FlagStr(p *string, name string, shortName rune, value string, usage string) { + s.flags = append(s.flags, Flag{name: name, shortName: string(shortName), acceptsVale: true}) flag.StringVar(p, name, value, usage) + + if string(shortName) != "" { + flag.StringVar(p, string(shortName), value, usage) + } } // printSynopsis returns back all the available flags without any description. diff --git a/internal/testutils/fake_file_helpers.go b/internal/testutils/fake_file_helpers.go index b1b8c7e..c1b38c5 100644 --- a/internal/testutils/fake_file_helpers.go +++ b/internal/testutils/fake_file_helpers.go @@ -146,3 +146,7 @@ func (fh FakeFilesHelper) CreateInitFiles() (*os.File, error) { func (fh FakeFilesHelper) GetLatestCreatedGoVersionDirectory() (string, error) { return "", nil } + +func (fh FakeFilesHelper) ReadVersionFromMod() (string, error) { + return "", nil +}