diff --git a/cmd/install.go b/cmd/install.go index e141cee..7b0ad47 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -25,16 +25,16 @@ type installOpts struct { progress bool } -func newInstall(installOpts installOpts, release string) error { +func newInstall(installOpts installOpts, release string) (error, string) { iv, err := config.GetInstalledVersions() if err != nil && !errors.Is(err, os.ErrNotExist) { - return err + return err, "" } if release == "" { client := gh.NewClient(os.Getenv("GITHUB_PAT")) - releases, err := gh.GetReleases(client, false) + releases, err := gh.GetReleases(client, 100) if err != nil { - return err + return err, "" } versions := []string{} for _, r := range releases { @@ -52,30 +52,43 @@ func newInstall(installOpts installOpts, release string) error { }, }, &release, survey.WithPageSize(10)) if err != nil { - return err + return err, "" } } - if release == "latest" { + + // github's latest release is literally their latest release, + // not the latest tagged version + if release == "release" { client := gh.NewClient(os.Getenv("GITHUB_PAT")) latestRelease, err := gh.GetLatestRelease(client) if err != nil { - return err + return err, "" } release = latestRelease.GetTagName() } + + if release == "latest" { + client := gh.NewClient(os.Getenv("GITHUB_PAT")) + releases, err := gh.GetReleases(client, 1) + if err != nil { + return err, "" + } + release = releases[0].GetTagName() + } + _, ok := iv[release] if ok { log.Infof("quarto version %s is already installed\n", release) - return nil + return nil, release } log.Info("attempting to install quarto version: ", release) res, err := pipeline.DownloadReleaseVersion(release, runtime.GOOS, installOpts.progress) if err != nil { - return err + return err, "" } log.Infof("new quarto version %s installed\n", release) log.Debugf("new quarto version installed to %s\n", res) - return nil + return nil, release } func setInstallOpts(installOpts *installOpts) { @@ -112,7 +125,7 @@ func newInstallCmd() *installCmd { wg.Add(1) go func(errc <-chan error, release string) { defer wg.Done() - err := newInstall(root.opts, release) + err, _ := newInstall(root.opts, release) errChan <- err }(errChan, arg) } diff --git a/cmd/ls.go b/cmd/ls.go index 884aac4..5ab75a2 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -20,19 +20,26 @@ type lsCmd struct { type lsOpts struct { remote bool + num int } func newLs(lsOpts lsOpts) error { if lsOpts.remote { client := gh.NewClient(os.Getenv("GITHUB_PAT")) - releases, err := gh.GetReleases(client, false) + releases, err := gh.GetReleases(client, lsOpts.num) if err != nil { return err } - fmt.Println("version | release date | description") + fmt.Println("version | release date | description | type") for _, r := range releases { createdAt := r.GetCreatedAt() - fmt.Printf("%s | %s | %s\n", r.GetTagName(), createdAt.Format("2006-01-02"), r.GetName()) + var releaseType string + if r.GetPrerelease() { + releaseType = "pre-release" + } else { + releaseType = "release" + } + fmt.Printf("%s | %s | %s | %s \n", r.GetTagName(), createdAt.Format("2006-01-02"), r.GetName(), releaseType) } } else { entries, err := os.ReadDir(config.GetPathToVersionsDir()) @@ -43,6 +50,10 @@ func newLs(lsOpts lsOpts) error { if err != nil { return err } + if len(entries) < lsOpts.num { + lsOpts.num = len(entries) + } + entries = entries[:lsOpts.num-1] // TODO: replace with actual table fmt.Println("version | install time") fmt.Println("--------------------------------") @@ -78,6 +89,7 @@ func newLs(lsOpts lsOpts) error { func setLsOpts(lsOpts *lsOpts) { lsOpts.remote = viper.GetBool("remote") + lsOpts.num = viper.GetInt("number") } func (opts *lsOpts) Validate() error { @@ -108,6 +120,8 @@ func newLsCmd() *lsCmd { } cmd.Flags().Bool("remote", false, "list remote versions") viper.BindPFlag("remote", cmd.Flags().Lookup("remote")) + cmd.Flags().IntP("number", "n", 10, "number of versions to list") + viper.BindPFlag("number", cmd.Flags().Lookup("number")) root.cmd = cmd return root } diff --git a/cmd/use.go b/cmd/use.go index 0095955..452f138 100644 --- a/cmd/use.go +++ b/cmd/use.go @@ -7,11 +7,15 @@ import ( "path/filepath" "runtime" "sort" + "strings" "github.com/AlecAivazis/survey/v2" + "github.com/coreos/go-semver/semver" "github.com/dpastoor/qvm/internal/config" + "github.com/dpastoor/qvm/internal/gh" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" "golang.org/x/exp/maps" ) @@ -21,6 +25,7 @@ type useCmd struct { } type useOpts struct { + install bool } func newUse(useOpts useOpts, version string) error { @@ -29,12 +34,54 @@ func newUse(useOpts useOpts, version string) error { return err } versions := maps.Keys(iv) - sort.Sort(sort.Reverse(sort.StringSlice(versions))) - if len(iv) == 0 { + var semVersions semver.Versions + // sorting has some issues given how the character values will present individually + // for example a high value double digit patch version will be sorted before a lower + // value triple digit patch version. For example, sorting shows ordering like: + // v1.2.89 v1.2.237 v1.2.112 v1.1.84 v1.1.251 v1.1.189 + // where .89 is > 237 + // using go-semver this works + // as will get 1.2.237 1.2.112 1.2.89 1.1.251 1.1.189 1.1.168 1.1.84 + for _, v := range versions { + ver, err := semver.NewVersion(strings.TrimPrefix(v, "v")) + if err != nil { + // we're just going to warn rather than error right now in case some + // releases end up not following semver and would rather the tool not blow up + log.Errorf("could not parse semver value for %s with err %s\n ", v, err) + continue + } + semVersions = append(semVersions, ver) + } + sort.Sort(sort.Reverse(semVersions)) + // note this could be a bug if ever we do get nonparseable versions thrown out above + // will cross that bridge if we get there + for i, v := range semVersions { + versions[i] = "v" + v.String() + } + // convert back to string for later options + if len(iv) == 0 && !useOpts.install { return errors.New("no installed versions found, please install a version first") } + client := gh.NewClient(os.Getenv("GITHUB_PAT")) + if version == "release" { + latestRelease, err := gh.GetLatestRelease(client) + if err != nil { + return err + } + version = latestRelease.GetTagName() + } if version == "latest" { - version = versions[0] + if useOpts.install { + // this will install further down if the version isn't already installed + repo, err := gh.GetReleases(client, 1) + if err != nil { + return err + } + version = repo[0].GetTagName() + } else { + version = versions[0] + } + // add back the v we trimmed for semver } if version == "" { // not worried about an error here as an active version of @@ -56,7 +103,14 @@ func newUse(useOpts useOpts, version string) error { } quartopath, ok := iv[version] if !ok { - return fmt.Errorf("version %s not found", version) + if useOpts.install { + err, version = newInstall(installOpts{progress: true}, version) + if err != nil { + return err + } + } else { + return fmt.Errorf("version %s not found", version) + } } err = os.MkdirAll(config.GetPathToActiveBinDir(), 0755) if err != nil { @@ -82,7 +136,7 @@ func newUse(useOpts useOpts, version string) error { } func setUseOpts(useOpts *useOpts) { - + useOpts.install = viper.GetBool("install") } func (opts *useOpts) Validate() error { @@ -115,6 +169,8 @@ func newUseCmd() *useCmd { return nil }, } + cmd.Flags().Bool("install", false, "install the version if not already installed") + viper.BindPFlag("install", cmd.Flags().Lookup("install")) root.cmd = cmd return root } diff --git a/go.mod b/go.mod index 9a99be9..ffbbe18 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/AlecAivazis/survey/v2 v2.3.5-0.20220530090844-e47352f91434 github.com/adrg/xdg v0.4.0 + github.com/coreos/go-semver v0.3.0 github.com/dustin/go-humanize v1.0.0 github.com/google/go-github/v44 v44.1.0 github.com/mholt/archiver/v4 v4.0.0-alpha.6.0.20220421032531-8a97d87612e9 diff --git a/go.sum b/go.sum index e1475d4..103338f 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= diff --git a/internal/gh/releases.go b/internal/gh/releases.go index d3283c5..2551c9c 100644 --- a/internal/gh/releases.go +++ b/internal/gh/releases.go @@ -32,11 +32,19 @@ func GetLatestRelease(client *github.Client) (*github.RepositoryRelease, error) return rel, err } -func GetReleases(client *github.Client, paginate bool) ([]*github.RepositoryRelease, error) { - opts := &github.ListOptions{PerPage: 50} +func GetReleases(client *github.Client, n int) ([]*github.RepositoryRelease, error) { + // max of 50 per page + perPage := 50 + remaining := n - perPage + if n < 50 { + remaining = 0 + perPage = n + } var releases []*github.RepositoryRelease + opts := &github.ListOptions{PerPage: perPage} for { start := time.Now() + log.Tracef("perpage: %d, remaining: %d", opts.PerPage, remaining) rel, resp, err := client.Repositories.ListReleases( context.Background(), "quarto-dev", @@ -48,9 +56,15 @@ func GetReleases(client *github.Client, paginate bool) ([]*github.RepositoryRele } releases = append(releases, rel...) log.Tracef("repository release paginator: %s, page: %d", time.Since(start), resp.NextPage) - if !paginate || resp.NextPage == 0 { + if remaining <= 0 || resp.NextPage == 0 { break } + if remaining <= perPage { + opts.PerPage = remaining + remaining = 0 + } else { + remaining -= perPage + } opts.Page = resp.NextPage } return releases, nil