From 400b4a6578e48df017bd6e17949d344555200eba Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Sat, 9 Dec 2023 12:27:49 +0300 Subject: [PATCH 1/2] feat: rearrange functions to files - private functions to helpers - public functions to gobrew - use os variables only on main function - setup gobrew with one function --- cmd/gobrew/main.go | 10 +- gobrew.go | 480 ++------------------------------------------- gobrew_test.go | 140 +------------ helpers.go | 459 ++++++++++++++++++++++++++++++++++++++++++- helpers_test.go | 80 +++++++- 5 files changed, 568 insertions(+), 601 deletions(-) diff --git a/cmd/gobrew/main.go b/cmd/gobrew/main.go index 1673663..6f1492f 100644 --- a/cmd/gobrew/main.go +++ b/cmd/gobrew/main.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/kevincobain2000/gobrew" + "github.com/kevincobain2000/gobrew/utils" ) var args []string @@ -73,7 +74,14 @@ func init() { } func main() { - gb := gobrew.NewGoBrew() + homeDir, ok := os.LookupEnv("GOBREW_ROOT") + if !ok { + var err error + homeDir, err = os.UserHomeDir() + utils.CheckError(err, "failed get home directory and GOBREW_ROOT not defined") + } + + gb := gobrew.NewGoBrew(homeDir) switch actionArg { case "interactive", "info": gb.Interactive(true) diff --git a/gobrew.go b/gobrew.go index 42a5ce3..b1bb38c 100644 --- a/gobrew.go +++ b/gobrew.go @@ -1,22 +1,17 @@ package gobrew import ( - "bufio" - "encoding/json" "fmt" "io" "io/fs" - "net/http" "os" "path/filepath" "regexp" - "runtime" "sort" "strconv" "strings" "github.com/Masterminds/semver" - "github.com/c4milo/unpackit" "github.com/gookit/color" "github.com/kevincobain2000/gobrew/utils" ) @@ -42,6 +37,7 @@ type Command interface { Prune() Version(currentVersion string) Upgrade(currentVersion string) + Interactive(ask bool) } // GoBrew struct @@ -56,44 +52,33 @@ type GoBrew struct { } // NewGoBrew instance -func NewGoBrew() GoBrew { - homeDir, err := os.UserHomeDir() - if err != nil { - homeDir = os.Getenv("HOME") - } - - if os.Getenv("GOBREW_ROOT") != "" { - homeDir = os.Getenv("GOBREW_ROOT") +func NewGoBrew(homeDir string) GoBrew { + installDir := filepath.Join(homeDir, goBrewDir) + gb := GoBrew{ + homeDir: homeDir, + installDir: installDir, + versionsDir: filepath.Join(installDir, "versions"), + currentDir: filepath.Join(installDir, "current"), + currentBinDir: filepath.Join(installDir, "current", "bin"), + currentGoDir: filepath.Join(installDir, "current", "go"), + downloadsDir: filepath.Join(installDir, "downloads"), } - return NewGoBrewDirectory(homeDir) -} - -func NewGoBrewDirectory(homeDir string) GoBrew { - gb := GoBrew{} - gb.homeDir = homeDir - - gb.installDir = filepath.Join(gb.homeDir, goBrewDir) - gb.versionsDir = filepath.Join(gb.installDir, "versions") - gb.currentDir = filepath.Join(gb.installDir, "current") - gb.currentBinDir = filepath.Join(gb.installDir, "current", "bin") - gb.currentGoDir = filepath.Join(gb.installDir, "current", "go") - gb.downloadsDir = filepath.Join(gb.installDir, "downloads") - return gb } +// Interactive used by default func (gb *GoBrew) Interactive(ask bool) { currentVersion := gb.CurrentVersion() - currentMajorVersion := ExtractMajorVersion(currentVersion) + currentMajorVersion := extractMajorVersion(currentVersion) latestVersion := gb.getLatestVersion() - latestMajorVersion := ExtractMajorVersion(latestVersion) + latestMajorVersion := extractMajorVersion(latestVersion) modVersion := "None" if gb.hasModFile() { modVersion = gb.getModVersion() - modVersion = ExtractMajorVersion(modVersion) + modVersion = extractMajorVersion(modVersion) } fmt.Println() @@ -130,7 +115,7 @@ func (gb *GoBrew) Interactive(ask bool) { color.Warnln("GO is not installed.") c := true if ask { - c = AskForConfirmation("Do you want to use latest GO version (" + latestVersion + ")?") + c = askForConfirmation("Do you want to use latest GO version (" + latestVersion + ")?") } if c { gb.Use(latestVersion) @@ -142,7 +127,7 @@ func (gb *GoBrew) Interactive(ask bool) { color.Warnf("GO Installed Version (%s) and go.mod Version (%s) are different.\n", currentMajorVersion, modVersion) c := true if ask { - c = AskForConfirmation("Do you want to use GO version same as go.mod version (" + modVersion + "@latest)?") + c = askForConfirmation("Do you want to use GO version same as go.mod version (" + modVersion + "@latest)?") } if c { gb.Use(modVersion + "@latest") @@ -154,7 +139,7 @@ func (gb *GoBrew) Interactive(ask bool) { color.Warnf("GO Installed Version (%s) and GO Latest Version (%s) are different.\n", currentVersion, latestVersion) c := true if ask { - c = AskForConfirmation("Do you want to update GO to latest version (" + latestVersion + ")?") + c = askForConfirmation("Do you want to update GO to latest version (" + latestVersion + ")?") } if c { gb.Use(latestVersion) @@ -163,22 +148,6 @@ func (gb *GoBrew) Interactive(ask bool) { } } -func (gb *GoBrew) getLatestVersion() string { - getGolangVersions := gb.getGolangVersions() - // loop through reverse and ignore beta and rc versions to get latest version - for i := len(getGolangVersions) - 1; i >= 0; i-- { - r := regexp.MustCompile("beta.*|rc.*") - matches := r.FindAllString(getGolangVersions[i], -1) - if len(matches) == 0 { - return getGolangVersions[i] - } - } - return "" -} -func (gb *GoBrew) getArch() string { - return runtime.GOOS + "-" + runtime.GOARCH -} - // Prune removes all installed versions of go except current version func (gb *GoBrew) Prune() { currentVersion := gb.CurrentVersion() @@ -285,133 +254,6 @@ func (gb *GoBrew) ListRemoteVersions(print bool) map[string][]string { return gb.getGroupedVersion(versions, print) } -func (gb *GoBrew) getGroupedVersion(versions []string, print bool) map[string][]string { - groupedVersions := make(map[string][]string) - for _, version := range versions { - parts := strings.Split(version, ".") - if len(parts) > 1 { - majorVersion := fmt.Sprintf("%s.%s", parts[0], parts[1]) - r := regexp.MustCompile("beta.*|rc.*") - matches := r.FindAllString(majorVersion, -1) - if len(matches) == 1 { - majorVersion = strings.Split(version, matches[0])[0] - } - if !isBlackListed(majorVersion) { - groupedVersions[majorVersion] = append(groupedVersions[majorVersion], version) - } - } - } - - // groupedVersionKeys := []string{"1", "1.1", "1.2", ..., "1.17"} - groupedVersionKeys := make([]string, 0, len(groupedVersions)) - for groupedVersionKey := range groupedVersions { - groupedVersionKeys = append(groupedVersionKeys, groupedVersionKey) - } - - versionsSemantic := make([]*semver.Version, 0) - for _, r := range groupedVersionKeys { - if v, err := semver.NewVersion(r); err == nil { - versionsSemantic = append(versionsSemantic, v) - } - } - - // sort semantic versions - sort.Sort(semver.Collection(versionsSemantic)) - - // match 1.0.0 or 2.0.0 - reTopVersion, _ := regexp.Compile("[0-9]+.0.0") - - for _, versionSemantic := range versionsSemantic { - maxPerLine := 0 - strKey := versionSemantic.String() - lookupKey := "" - versionParts := strings.Split(strKey, ".") - - // prepare lookup key for the grouped version map. - // 1.0.0 -> 1.0, 1.1.1 -> 1.1 - lookupKey = versionParts[0] + "." + versionParts[1] - // On match 1.0.0, print 1. On match 2.0.0 print 2 - if reTopVersion.MatchString(strKey) { - if print { - color.Infop(versionParts[0]) - } - gb.print("\t", print) - } else { - if print { - color.Successp(lookupKey) - } - gb.print("\t", print) - } - - groupedVersionsSemantic := make([]*semver.Version, 0) - for _, r := range groupedVersions[lookupKey] { - if v, err := semver.NewVersion(r); err == nil { - groupedVersionsSemantic = append(groupedVersionsSemantic, v) - } - - } - // sort semantic versions - sort.Sort(semver.Collection(groupedVersionsSemantic)) - - for _, gvSemantic := range groupedVersionsSemantic { - maxPerLine++ - if maxPerLine == 6 { - maxPerLine = 0 - gb.print("\n\t", print) - } - gb.print(gvSemantic.String()+" ", print) - } - - maxPerLine = 0 - gb.print("\n\t", print) - - // print rc and beta versions in the end - for _, rcVersion := range groupedVersions[lookupKey] { - r := regexp.MustCompile("beta.*|rc.*") - matches := r.FindAllString(rcVersion, -1) - if len(matches) == 1 { - gb.print(rcVersion+" ", print) - maxPerLine++ - if maxPerLine == 6 { - maxPerLine = 0 - gb.print("\n\t", print) - } - } - } - gb.print("\n", print) - gb.print("\n", print) - } - return groupedVersions -} - -func isBlackListed(version string) bool { - blackListVersions := []string{"1.0", "1.1", "1.2", "1.3", "1.4"} - for _, v := range blackListVersions { - if version == v { - return true - } - } - return false -} - -func (gb *GoBrew) print(message string, shouldPrint bool) { - if shouldPrint { - color.Infop(message) - } -} - -func (gb *GoBrew) existsVersion(version string) bool { - path := filepath.Join(gb.versionsDir, version, "go") - _, err := os.Stat(path) - if err == nil { - return true - } - if os.IsNotExist(err) { - return false - } - return false -} - // CurrentVersion get current version from symb link func (gb *GoBrew) CurrentVersion() string { fp, err := filepath.EvalSymlinks(gb.currentBinDir) @@ -441,14 +283,6 @@ func (gb *GoBrew) Uninstall(version string) { color.Successf("==> [Success] Version: %s uninstalled\n", version) } -func (gb *GoBrew) cleanVersionDir(version string) { - _ = os.RemoveAll(gb.getVersionDir(version)) -} - -func (gb *GoBrew) cleanDownloadsDir() { - _ = os.RemoveAll(gb.downloadsDir) -} - // Install the given version of go func (gb *GoBrew) Install(version string) string { if version == "" || version == "None" { @@ -469,139 +303,6 @@ func (gb *GoBrew) Install(version string) string { return version } -func (gb *GoBrew) judgeVersion(version string) string { - judgedVersion := "None" - rcBetaOk := false - reRcOrBeta := regexp.MustCompile("beta.*|rc.*") - - // check if version string ends with x - if strings.HasSuffix(version, "x") { - judgedVersion = strings.TrimSuffix(version, "x") - } - - if strings.HasSuffix(version, ".x") { - judgedVersion = strings.TrimSuffix(version, ".x") - } - if strings.HasSuffix(version, "@latest") { - judgedVersion = strings.TrimSuffix(version, "@latest") - } - if strings.HasSuffix(version, "@dev-latest") { - judgedVersion = strings.TrimSuffix(version, "@dev-latest") - rcBetaOk = true - } - - if version == "mod" { - // get version by reading the mod file of Go - modVersion := gb.getModVersion() - // if modVersion is like 1.19, 1.20, 1.21 then appened @latest to it - if strings.Count(modVersion, ".") == 1 { - modVersion = modVersion + "@latest" - } - return gb.judgeVersion(modVersion) - } - groupedVersions := gb.ListRemoteVersions(false) // donot print - if version == "latest" || version == "dev-latest" { - groupedVersionKeys := make([]string, 0, len(groupedVersions)) - for groupedVersionKey := range groupedVersions { - groupedVersionKeys = append(groupedVersionKeys, groupedVersionKey) - } - versionsSemantic := make([]*semver.Version, 0) - for _, r := range groupedVersionKeys { - if v, err := semver.NewVersion(r); err == nil { - versionsSemantic = append(versionsSemantic, v) - } - } - if len(versionsSemantic) == 0 { - return "None" - } - - // sort semantic versions - sort.Sort(semver.Collection(versionsSemantic)) - // loop in reverse - for i := len(versionsSemantic) - 1; i >= 0; i-- { - judgedVersions := groupedVersions[versionsSemantic[i].Original()] - // get last element - if version == "dev-latest" { - if len(judgedVersions) == 0 { - return "None" - } - return judgedVersions[len(judgedVersions)-1] - } - - // loop in reverse - for j := len(judgedVersions) - 1; j >= 0; j-- { - matches := reRcOrBeta.FindAllString(judgedVersions[j], -1) - if len(matches) == 0 { - return judgedVersions[j] - } - } - } - - latest := versionsSemantic[len(versionsSemantic)-1].String() - return gb.judgeVersion(latest) - } - - if judgedVersion != "None" { - // check if judgedVersion is in the groupedVersions - if _, ok := groupedVersions[judgedVersion]; ok { - // get last item in the groupedVersions excluding rc and beta - // loop in reverse groupedVersions - for i := len(groupedVersions[judgedVersion]) - 1; i >= 0; i-- { - matches := reRcOrBeta.FindAllString(groupedVersions[judgedVersion][i], -1) - if len(matches) == 0 { - return groupedVersions[judgedVersion][i] - } - } - if rcBetaOk { - // return last element including beta and rc if present - return groupedVersions[judgedVersion][len(groupedVersions[judgedVersion])-1] - } - } - } - - return version -} - -func (gb *GoBrew) hasModFile() bool { - modFilePath := filepath.Join("go.mod") - _, err := os.Stat(modFilePath) - if err == nil { - return true - } - if os.IsNotExist(err) { - return false - } - return false -} - -// read go.mod file and extract version -// Do not use go to get the version as go list -m -f '{{.GoVersion}}' -// Because go might not be installed -func (gb *GoBrew) getModVersion() string { - modFilePath := filepath.Join("go.mod") - modFile, err := os.Open(modFilePath) - if err != nil { - return "None" - } - defer func(modFile *os.File) { - _ = modFile.Close() - }(modFile) - - scanner := bufio.NewScanner(modFile) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "go ") { - return strings.TrimPrefix(line, "go ") - } - } - - if err = scanner.Err(); err != nil { - color.Errorln(err) - os.Exit(1) - } - return "None" -} - // Use a version func (gb *GoBrew) Use(version string) { version = gb.Install(version) @@ -654,148 +355,3 @@ func (gb *GoBrew) Upgrade(currentVersion string) { utils.CheckError(os.Chmod(goBrewFile, 0755), "==> [Error] Cannot set file as executable") color.Infoln("Upgrade successful") } - -func (gb *GoBrew) mkDirs(version string) { - _ = os.MkdirAll(gb.installDir, os.ModePerm) - _ = os.MkdirAll(gb.currentDir, os.ModePerm) - _ = os.MkdirAll(gb.versionsDir, os.ModePerm) - _ = os.MkdirAll(gb.getVersionDir(version), os.ModePerm) - _ = os.MkdirAll(gb.downloadsDir, os.ModePerm) -} - -func (gb *GoBrew) getVersionDir(version string) string { - return filepath.Join(gb.versionsDir, version) -} - -func (gb *GoBrew) downloadAndExtract(version string) { - tarName := "go" + version + "." + gb.getArch() + tarNameExt - - registryPath := defaultRegistryPath - if p := os.Getenv("GOBREW_REGISTRY"); p != "" { - registryPath = p - } - downloadURL := registryPath + tarName - color.Infoln("==> [Info] Downloading from:", downloadURL) - - dstDownloadDir := filepath.Join(gb.downloadsDir) - color.Infoln("==> [Info] Downloading to:", dstDownloadDir) - err := utils.DownloadWithProgress(downloadURL, tarName, dstDownloadDir) - - if err != nil { - gb.cleanVersionDir(version) - color.Infoln("==> [Info] Downloading version failed:", err) - color.Errorln("==> [Error]: Please check connectivity to url:", downloadURL) - os.Exit(1) - } - - srcTar := filepath.Join(gb.downloadsDir, tarName) - dstDir := gb.getVersionDir(version) - - color.Infoln("==> [Info] Extracting from:", srcTar) - color.Infoln("==> [Info] Extracting to:", dstDir) - - err = gb.Extract(srcTar, dstDir) - if err != nil { - // clean up dir - gb.cleanVersionDir(version) - color.Infoln("==> [Info] Extract failed:", err) - color.Errorln("==> [Error]: Please check if version exists from url:", downloadURL) - os.Exit(1) - } - color.Infoln("[Success] Extract to", gb.getVersionDir(version)) -} - -func (gb *GoBrew) Extract(srcTar string, dstDir string) error { - //#nosec G304 - file, err := os.Open(srcTar) - if err != nil { - return err - } - err = unpackit.Unpack(file, dstDir) - if err != nil { - return err - } - - return nil -} - -func (gb *GoBrew) changeSymblinkGoBin(version string) { - goBinDst := filepath.Join(gb.versionsDir, version, "/go/bin") - _ = os.RemoveAll(gb.currentBinDir) - utils.CheckError(os.Symlink(goBinDst, gb.currentBinDir), "==> [Error]: symbolic link failed") -} - -func (gb *GoBrew) changeSymblinkGo(version string) { - _ = os.RemoveAll(gb.currentGoDir) - versionGoDir := filepath.Join(gb.versionsDir, version, "go") - utils.CheckError(os.Symlink(versionGoDir, gb.currentGoDir), "==> [Error]: symbolic link failed") -} - -func (gb *GoBrew) getGobrewVersion() string { - url := "https://api.github.com/repos/kevincobain2000/gobrew/releases/latest" - data := doRequest(url) - if len(data) == 0 { - return "" - } - - type Tag struct { - TagName string `json:"tag_name"` - } - var tag Tag - utils.CheckError(json.Unmarshal(data, &tag), "==> [Error]") - - return tag.TagName -} - -func (gb *GoBrew) getGolangVersions() (result []string) { - data := doRequest(goBrewTagsApi) - if len(data) == 0 { - return - } - - type Tag struct { - Ref string `json:"ref"` - } - var tags []Tag - utils.CheckError(json.Unmarshal(data, &tags), "==> [Error]") - - for _, tag := range tags { - t := strings.ReplaceAll(tag.Ref, "refs/tags/", "") - if strings.HasPrefix(t, "go") { - result = append(result, strings.TrimPrefix(t, "go")) - } - } - - return -} - -func doRequest(url string) (data []byte) { - client := &http.Client{} - request, err := http.NewRequest("GET", url, nil) - utils.CheckError(err, "==> [Error] Cannot create request") - - request.Header.Set("User-Agent", "gobrew") - - response, err := client.Do(request) - utils.CheckError(err, "==> [Error] Cannot get response") - - defer func(body io.ReadCloser) { - _ = body.Close() - }(response.Body) - - if response.StatusCode == http.StatusTooManyRequests || - response.StatusCode == http.StatusForbidden { - color.Errorln("==> [Error] Rate limit exhausted") - os.Exit(1) - } - - if response.StatusCode != http.StatusOK { - color.Errorln("==> [Error] Cannot read response:", response.Status) - os.Exit(1) - } - - data, err = io.ReadAll(response.Body) - utils.CheckError(err, "==> [Error] Cannot read response Body:") - - return -} diff --git a/gobrew_test.go b/gobrew_test.go index 1d76c03..5304512 100644 --- a/gobrew_test.go +++ b/gobrew_test.go @@ -3,132 +3,15 @@ package gobrew import ( "os" "path/filepath" - "runtime" "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestNewGobrewHomeDirUsesUserHomeDir(t *testing.T) { - t.Parallel() - homeDir, err := os.UserHomeDir() - - if err != nil { - t.FailNow() - } - - gobrew := NewGoBrew() - - assert.Equal(t, homeDir, gobrew.homeDir) - t.Log("test finished") -} - -func TestNewGobrewHomeDirDefaultsToHome(t *testing.T) { - var envName string - - switch runtime.GOOS { - case "windows": - envName = "USERPROFILE" - case "plan9": - envName = "home" - default: - envName = "HOME" - } - - t.Setenv(envName, "") - gobrew := NewGoBrew() - - assert.Equal(t, os.Getenv("HOME"), gobrew.homeDir) - t.Log("test finished") -} - -func TestNewGobrewHomeDirUsesGoBrewRoot(t *testing.T) { - t.Setenv("GOBREW_ROOT", "some_fancy_value") - gobrew := NewGoBrew() - assert.Equal(t, "some_fancy_value", gobrew.homeDir) - t.Log("test finished") -} - -func TestJudgeVersion(t *testing.T) { - t.Parallel() - tests := []struct { - version string - wantVersion string - wantError error - }{ - { - version: "1.8", - wantVersion: "1.8", - }, - { - version: "1.8.2", - wantVersion: "1.8.2", - }, - { - version: "1.18beta1", - wantVersion: "1.18beta1", - }, - { - version: "1.18rc1", - wantVersion: "1.18rc1", - }, - { - version: "1.18@latest", - wantVersion: "1.18.10", - }, - { - version: "1.18@dev-latest", - wantVersion: "1.18.10", - }, - // following 2 tests fail upon new version release - // commenting out for now as the tool is stable - // { - // version: "latest", - // wantVersion: "1.19.1", - // }, - // { - // version: "dev-latest", - // wantVersion: "1.19.1", - // }, - } - for _, test := range tests { - test := test - t.Run(test.version, func(t *testing.T) { - t.Parallel() - gb := NewGoBrew() - version := gb.judgeVersion(test.version) - assert.Equal(t, test.wantVersion, version) - - }) - } - t.Log("test finished") -} - -func TestListVersions(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() - gb := NewGoBrewDirectory(tempDir) - - gb.ListVersions() - t.Log("test finished") -} - -func TestExistVersion(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() - gb := NewGoBrewDirectory(tempDir) - - exists := gb.existsVersion("1.19") - - assert.Equal(t, false, exists) - t.Log("test finished") -} - func TestInstallAndExistVersion(t *testing.T) { t.Parallel() - tempDir := t.TempDir() - gb := NewGoBrewDirectory(tempDir) + gb := NewGoBrew(t.TempDir()) gb.Install("1.19") exists := gb.existsVersion("1.19") assert.Equal(t, true, exists) @@ -137,8 +20,7 @@ func TestInstallAndExistVersion(t *testing.T) { func TestUnInstallThenNotExistVersion(t *testing.T) { t.Parallel() - tempDir := t.TempDir() - gb := NewGoBrewDirectory(tempDir) + gb := NewGoBrew(t.TempDir()) gb.Uninstall("1.19") exists := gb.existsVersion("1.19") assert.Equal(t, false, exists) @@ -147,9 +29,7 @@ func TestUnInstallThenNotExistVersion(t *testing.T) { func TestUpgrade(t *testing.T) { t.Parallel() - tempDir := t.TempDir() - - gb := NewGoBrewDirectory(tempDir) + gb := NewGoBrew(t.TempDir()) binaryDir := filepath.Join(gb.installDir, "bin") _ = os.MkdirAll(binaryDir, os.ModePerm) @@ -172,9 +52,7 @@ func TestUpgrade(t *testing.T) { func TestDoNotUpgradeLatestVersion(t *testing.T) { t.Skip("skipping test...needs to rewrite") - tempDir := t.TempDir() - - gb := NewGoBrewDirectory(tempDir) + gb := NewGoBrew(t.TempDir()) binaryDir := filepath.Join(gb.installDir, "bin") _ = os.MkdirAll(binaryDir, os.ModePerm) @@ -198,9 +76,7 @@ func TestDoNotUpgradeLatestVersion(t *testing.T) { func TestInteractive(t *testing.T) { t.Parallel() - tempDir := t.TempDir() - - gb := NewGoBrewDirectory(tempDir) + gb := NewGoBrew(t.TempDir()) currentVersion := gb.CurrentVersion() latestVersion := gb.getLatestVersion() // modVersion := gb.getModVersion() @@ -230,8 +106,7 @@ func TestInteractive(t *testing.T) { func TestPrune(t *testing.T) { t.Parallel() - tempDir := t.TempDir() - gb := NewGoBrewDirectory(tempDir) + gb := NewGoBrew(t.TempDir()) gb.Install("1.20") gb.Install("1.19") gb.Use("1.19") @@ -243,8 +118,7 @@ func TestPrune(t *testing.T) { func TestGoBrew_CurrentVersion(t *testing.T) { t.Parallel() - tempDir := t.TempDir() - gb := NewGoBrewDirectory(tempDir) + gb := NewGoBrew(t.TempDir()) assert.Equal(t, true, gb.CurrentVersion() == "None") gb.Install("1.19") gb.Use("1.19") diff --git a/helpers.go b/helpers.go index 4a6eb95..88d1ab6 100644 --- a/helpers.go +++ b/helpers.go @@ -2,15 +2,465 @@ package gobrew import ( "bufio" + "encoding/json" "fmt" + "io" "log" + "net" + "net/http" "os" + "path/filepath" + "regexp" + "runtime" + "sort" "strings" + "time" + "github.com/Masterminds/semver" + "github.com/c4milo/unpackit" "github.com/gookit/color" + "github.com/kevincobain2000/gobrew/utils" ) -func ExtractMajorVersion(version string) string { +func (gb *GoBrew) getLatestVersion() string { + getGolangVersions := gb.getGolangVersions() + // loop through reverse and ignore beta and rc versions to get latest version + for i := len(getGolangVersions) - 1; i >= 0; i-- { + r := regexp.MustCompile("beta.*|rc.*") + matches := r.FindAllString(getGolangVersions[i], -1) + if len(matches) == 0 { + return getGolangVersions[i] + } + } + return "" +} + +func (gb *GoBrew) getArch() string { + return runtime.GOOS + "-" + runtime.GOARCH +} + +func (gb *GoBrew) getGroupedVersion(versions []string, print bool) map[string][]string { + groupedVersions := make(map[string][]string) + for _, version := range versions { + parts := strings.Split(version, ".") + if len(parts) > 1 { + majorVersion := fmt.Sprintf("%s.%s", parts[0], parts[1]) + r := regexp.MustCompile("beta.*|rc.*") + matches := r.FindAllString(majorVersion, -1) + if len(matches) == 1 { + majorVersion = strings.Split(version, matches[0])[0] + } + if !isBlackListed(majorVersion) { + groupedVersions[majorVersion] = append(groupedVersions[majorVersion], version) + } + } + } + + // groupedVersionKeys := []string{"1", "1.1", "1.2", ..., "1.17"} + groupedVersionKeys := make([]string, 0, len(groupedVersions)) + for groupedVersionKey := range groupedVersions { + groupedVersionKeys = append(groupedVersionKeys, groupedVersionKey) + } + + versionsSemantic := make([]*semver.Version, 0) + for _, r := range groupedVersionKeys { + if v, err := semver.NewVersion(r); err == nil { + versionsSemantic = append(versionsSemantic, v) + } + } + + // sort semantic versions + sort.Sort(semver.Collection(versionsSemantic)) + + // match 1.0.0 or 2.0.0 + reTopVersion, _ := regexp.Compile("[0-9]+.0.0") + + for _, versionSemantic := range versionsSemantic { + maxPerLine := 0 + strKey := versionSemantic.String() + lookupKey := "" + versionParts := strings.Split(strKey, ".") + + // prepare lookup key for the grouped version map. + // 1.0.0 -> 1.0, 1.1.1 -> 1.1 + lookupKey = versionParts[0] + "." + versionParts[1] + // On match 1.0.0, print 1. On match 2.0.0 print 2 + if reTopVersion.MatchString(strKey) { + if print { + color.Infop(versionParts[0]) + } + gb.print("\t", print) + } else { + if print { + color.Successp(lookupKey) + } + gb.print("\t", print) + } + + groupedVersionsSemantic := make([]*semver.Version, 0) + for _, r := range groupedVersions[lookupKey] { + if v, err := semver.NewVersion(r); err == nil { + groupedVersionsSemantic = append(groupedVersionsSemantic, v) + } + + } + // sort semantic versions + sort.Sort(semver.Collection(groupedVersionsSemantic)) + + for _, gvSemantic := range groupedVersionsSemantic { + maxPerLine++ + if maxPerLine == 6 { + maxPerLine = 0 + gb.print("\n\t", print) + } + gb.print(gvSemantic.String()+" ", print) + } + + maxPerLine = 0 + gb.print("\n\t", print) + + // print rc and beta versions in the end + for _, rcVersion := range groupedVersions[lookupKey] { + r := regexp.MustCompile("beta.*|rc.*") + matches := r.FindAllString(rcVersion, -1) + if len(matches) == 1 { + gb.print(rcVersion+" ", print) + maxPerLine++ + if maxPerLine == 6 { + maxPerLine = 0 + gb.print("\n\t", print) + } + } + } + gb.print("\n", print) + gb.print("\n", print) + } + return groupedVersions +} + +func isBlackListed(version string) bool { + blackListVersions := []string{"1.0", "1.1", "1.2", "1.3", "1.4"} + for _, v := range blackListVersions { + if version == v { + return true + } + } + return false +} + +func (gb *GoBrew) print(message string, shouldPrint bool) { + if shouldPrint { + color.Infop(message) + } +} + +func (gb *GoBrew) existsVersion(version string) bool { + path := filepath.Join(gb.versionsDir, version, "go") + _, err := os.Stat(path) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return false +} + +func (gb *GoBrew) cleanVersionDir(version string) { + _ = os.RemoveAll(gb.getVersionDir(version)) +} + +func (gb *GoBrew) cleanDownloadsDir() { + _ = os.RemoveAll(gb.downloadsDir) +} + +func (gb *GoBrew) judgeVersion(version string) string { + judgedVersion := "None" + rcBetaOk := false + reRcOrBeta := regexp.MustCompile("beta.*|rc.*") + + // check if version string ends with x + if strings.HasSuffix(version, "x") { + judgedVersion = strings.TrimSuffix(version, "x") + } + + if strings.HasSuffix(version, ".x") { + judgedVersion = strings.TrimSuffix(version, ".x") + } + if strings.HasSuffix(version, "@latest") { + judgedVersion = strings.TrimSuffix(version, "@latest") + } + if strings.HasSuffix(version, "@dev-latest") { + judgedVersion = strings.TrimSuffix(version, "@dev-latest") + rcBetaOk = true + } + + if version == "mod" { + // get version by reading the mod file of Go + modVersion := gb.getModVersion() + // if modVersion is like 1.19, 1.20, 1.21 then appened @latest to it + if strings.Count(modVersion, ".") == 1 { + modVersion = modVersion + "@latest" + } + return gb.judgeVersion(modVersion) + } + groupedVersions := gb.ListRemoteVersions(false) // donot print + if version == "latest" || version == "dev-latest" { + groupedVersionKeys := make([]string, 0, len(groupedVersions)) + for groupedVersionKey := range groupedVersions { + groupedVersionKeys = append(groupedVersionKeys, groupedVersionKey) + } + versionsSemantic := make([]*semver.Version, 0) + for _, r := range groupedVersionKeys { + if v, err := semver.NewVersion(r); err == nil { + versionsSemantic = append(versionsSemantic, v) + } + } + if len(versionsSemantic) == 0 { + return "None" + } + + // sort semantic versions + sort.Sort(semver.Collection(versionsSemantic)) + // loop in reverse + for i := len(versionsSemantic) - 1; i >= 0; i-- { + judgedVersions := groupedVersions[versionsSemantic[i].Original()] + // get last element + if version == "dev-latest" { + if len(judgedVersions) == 0 { + return "None" + } + return judgedVersions[len(judgedVersions)-1] + } + + // loop in reverse + for j := len(judgedVersions) - 1; j >= 0; j-- { + matches := reRcOrBeta.FindAllString(judgedVersions[j], -1) + if len(matches) == 0 { + return judgedVersions[j] + } + } + } + + latest := versionsSemantic[len(versionsSemantic)-1].String() + return gb.judgeVersion(latest) + } + + if judgedVersion != "None" { + // check if judgedVersion is in the groupedVersions + if _, ok := groupedVersions[judgedVersion]; ok { + // get last item in the groupedVersions excluding rc and beta + // loop in reverse groupedVersions + for i := len(groupedVersions[judgedVersion]) - 1; i >= 0; i-- { + matches := reRcOrBeta.FindAllString(groupedVersions[judgedVersion][i], -1) + if len(matches) == 0 { + return groupedVersions[judgedVersion][i] + } + } + if rcBetaOk { + // return last element including beta and rc if present + return groupedVersions[judgedVersion][len(groupedVersions[judgedVersion])-1] + } + } + } + + return version +} + +func (gb *GoBrew) hasModFile() bool { + modFilePath := filepath.Join("go.mod") + _, err := os.Stat(modFilePath) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return false +} + +// read go.mod file and extract version +// Do not use go to get the version as go list -m -f '{{.GoVersion}}' +// Because go might not be installed +func (gb *GoBrew) getModVersion() string { + modFilePath := filepath.Join("go.mod") + modFile, err := os.Open(modFilePath) + if err != nil { + return "None" + } + defer func(modFile *os.File) { + _ = modFile.Close() + }(modFile) + + scanner := bufio.NewScanner(modFile) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "go ") { + return strings.TrimPrefix(line, "go ") + } + } + + if err = scanner.Err(); err != nil { + color.Errorln(err) + os.Exit(1) + } + return "None" +} + +func (gb *GoBrew) mkDirs(version string) { + _ = os.MkdirAll(gb.installDir, os.ModePerm) + _ = os.MkdirAll(gb.currentDir, os.ModePerm) + _ = os.MkdirAll(gb.versionsDir, os.ModePerm) + _ = os.MkdirAll(gb.getVersionDir(version), os.ModePerm) + _ = os.MkdirAll(gb.downloadsDir, os.ModePerm) +} + +func (gb *GoBrew) getVersionDir(version string) string { + return filepath.Join(gb.versionsDir, version) +} + +func (gb *GoBrew) downloadAndExtract(version string) { + tarName := "go" + version + "." + gb.getArch() + tarNameExt + + registryPath := defaultRegistryPath + if p := os.Getenv("GOBREW_REGISTRY"); p != "" { + registryPath = p + } + downloadURL := registryPath + tarName + color.Infoln("==> [Info] Downloading from:", downloadURL) + + dstDownloadDir := filepath.Join(gb.downloadsDir) + color.Infoln("==> [Info] Downloading to:", dstDownloadDir) + err := utils.DownloadWithProgress(downloadURL, tarName, dstDownloadDir) + + if err != nil { + gb.cleanVersionDir(version) + color.Infoln("==> [Info] Downloading version failed:", err) + color.Errorln("==> [Error]: Please check connectivity to url:", downloadURL) + os.Exit(1) + } + + srcTar := filepath.Join(gb.downloadsDir, tarName) + dstDir := gb.getVersionDir(version) + + color.Infoln("==> [Info] Extracting from:", srcTar) + color.Infoln("==> [Info] Extracting to:", dstDir) + + err = gb.extract(srcTar, dstDir) + if err != nil { + // clean up dir + gb.cleanVersionDir(version) + color.Infoln("==> [Info] Extract failed:", err) + color.Errorln("==> [Error]: Please check if version exists from url:", downloadURL) + os.Exit(1) + } + color.Infoln("[Success] Extract to", gb.getVersionDir(version)) +} + +func (gb *GoBrew) changeSymblinkGoBin(version string) { + goBinDst := filepath.Join(gb.versionsDir, version, "/go/bin") + _ = os.RemoveAll(gb.currentBinDir) + utils.CheckError(os.Symlink(goBinDst, gb.currentBinDir), "==> [Error]: symbolic link failed") +} + +func (gb *GoBrew) changeSymblinkGo(version string) { + _ = os.RemoveAll(gb.currentGoDir) + versionGoDir := filepath.Join(gb.versionsDir, version, "go") + utils.CheckError(os.Symlink(versionGoDir, gb.currentGoDir), "==> [Error]: symbolic link failed") +} + +func (gb *GoBrew) getGobrewVersion() string { + url := "https://api.github.com/repos/kevincobain2000/gobrew/releases/latest" + data := doRequest(url) + if len(data) == 0 { + return "" + } + + type Tag struct { + TagName string `json:"tag_name"` + } + var tag Tag + utils.CheckError(json.Unmarshal(data, &tag), "==> [Error]") + + return tag.TagName +} + +func (gb *GoBrew) getGolangVersions() (result []string) { + data := doRequest(goBrewTagsApi) + if len(data) == 0 { + return + } + + type Tag struct { + Ref string `json:"ref"` + } + var tags []Tag + utils.CheckError(json.Unmarshal(data, &tags), "==> [Error]") + + for _, tag := range tags { + t := strings.ReplaceAll(tag.Ref, "refs/tags/", "") + if strings.HasPrefix(t, "go") { + result = append(result, strings.TrimPrefix(t, "go")) + } + } + + return +} + +func doRequest(url string) (data []byte) { + client := &http.Client{ + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 5 * time.Second, + }).Dial, + TLSHandshakeTimeout: 5 * time.Second, + }, + Timeout: 10 * time.Second, + } + request, err := http.NewRequest("GET", url, nil) + utils.CheckError(err, "==> [Error] Cannot create request") + + request.Header.Set("User-Agent", "gobrew") + + response, err := client.Do(request) + utils.CheckError(err, "==> [Error] Cannot get response") + + defer func(body io.ReadCloser) { + _ = body.Close() + }(response.Body) + + if response.StatusCode == http.StatusTooManyRequests || + response.StatusCode == http.StatusForbidden { + color.Errorln("==> [Error] Rate limit exhausted") + os.Exit(1) + } + + if response.StatusCode != http.StatusOK { + color.Errorln("==> [Error] Cannot read response:", response.Status) + os.Exit(1) + } + + data, err = io.ReadAll(response.Body) + utils.CheckError(err, "==> [Error] Cannot read response Body:") + + return +} + +func (gb *GoBrew) extract(srcTar string, dstDir string) error { + //#nosec G304 + file, err := os.Open(srcTar) + if err != nil { + return err + } + err = unpackit.Unpack(file, dstDir) + if err != nil { + return err + } + + return nil +} + +func extractMajorVersion(version string) string { parts := strings.Split(version, ".") if len(parts) < 2 { return "" @@ -24,7 +474,7 @@ func ExtractMajorVersion(version string) string { return majorVersion } -func AskForConfirmation(s string) bool { +func askForConfirmation(s string) bool { reader := bufio.NewReader(os.Stdin) for { @@ -38,9 +488,10 @@ func AskForConfirmation(s string) bool { response = strings.ToLower(strings.TrimSpace(response)) - if response == "y" || response == "yes" { + switch response { + case "y", "yes": return true - } else if response == "" || response == "n" || response == "no" { + case "", "n", "no": return false } } diff --git a/helpers_test.go b/helpers_test.go index 85a3fc4..97207e1 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -2,9 +2,85 @@ package gobrew import ( "testing" + + "github.com/stretchr/testify/assert" ) +func TestJudgeVersion(t *testing.T) { + t.Parallel() + tests := []struct { + version string + wantVersion string + wantError error + }{ + { + version: "1.8", + wantVersion: "1.8", + }, + { + version: "1.8.2", + wantVersion: "1.8.2", + }, + { + version: "1.18beta1", + wantVersion: "1.18beta1", + }, + { + version: "1.18rc1", + wantVersion: "1.18rc1", + }, + { + version: "1.18@latest", + wantVersion: "1.18.10", + }, + { + version: "1.18@dev-latest", + wantVersion: "1.18.10", + }, + // following 2 tests fail upon new version release + // commenting out for now as the tool is stable + // { + // version: "latest", + // wantVersion: "1.19.1", + // }, + // { + // version: "dev-latest", + // wantVersion: "1.19.1", + // }, + } + for _, test := range tests { + test := test + t.Run(test.version, func(t *testing.T) { + t.Parallel() + gb := NewGoBrew(t.TempDir()) + version := gb.judgeVersion(test.version) + assert.Equal(t, test.wantVersion, version) + + }) + } + t.Log("test finished") +} + +func TestListVersions(t *testing.T) { + t.Parallel() + gb := NewGoBrew(t.TempDir()) + + gb.ListVersions() + t.Log("test finished") +} + +func TestExistVersion(t *testing.T) { + t.Parallel() + gb := NewGoBrew(t.TempDir()) + + exists := gb.existsVersion("1.19") + + assert.Equal(t, false, exists) + t.Log("test finished") +} + func TestExtractMajorVersion(t *testing.T) { + t.Parallel() type args struct { version string } @@ -50,8 +126,10 @@ func TestExtractMajorVersion(t *testing.T) { }, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { - if got := ExtractMajorVersion(tt.args.version); got != tt.want { + t.Parallel() + if got := extractMajorVersion(tt.args.version); got != tt.want { t.Errorf("ExtractMajorVersion() = %v, want %v", got, tt.want) } }) From 95e7c082af43a56cda81417b2cb7b488902f464d Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Sat, 9 Dec 2023 12:36:01 +0300 Subject: [PATCH 2/2] feat: check homeDir maybe empty --- cmd/gobrew/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/gobrew/main.go b/cmd/gobrew/main.go index 6f1492f..172967e 100644 --- a/cmd/gobrew/main.go +++ b/cmd/gobrew/main.go @@ -75,7 +75,7 @@ func init() { func main() { homeDir, ok := os.LookupEnv("GOBREW_ROOT") - if !ok { + if !ok || homeDir == "" { var err error homeDir, err = os.UserHomeDir() utils.CheckError(err, "failed get home directory and GOBREW_ROOT not defined")