Skip to content

Commit

Permalink
Merge pull request #407 from exercism/refactor-upgrade
Browse files Browse the repository at this point in the history
Refactor upgrade command
  • Loading branch information
Katrina Owen authored Aug 1, 2017
2 parents 3968e88 + 473d779 commit 54de9de
Show file tree
Hide file tree
Showing 16 changed files with 292 additions and 245 deletions.
18 changes: 4 additions & 14 deletions cmd/release_utils.go → cli/asset.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package cmd
package cli

import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
)

type asset struct {
// Asset is a build for a particular system, uploaded to a GitHub release.
type Asset struct {
ID int `json:"id"`
Name string `json:"name"`
ContentType string `json:"content_type"`
}

func (a *asset) download() (*bytes.Reader, error) {
func (a *Asset) download() (*bytes.Reader, error) {
downloadURL := fmt.Sprintf("https://api.github.com/repos/exercism/cli/releases/assets/%d", a.ID)
req, err := http.NewRequest("GET", downloadURL, nil)
if err != nil {
Expand All @@ -35,13 +35,3 @@ func (a *asset) download() (*bytes.Reader, error) {

return bytes.NewReader(bs), nil
}

type release struct {
Location string `json:"html_url"`
TagName string `json:"tag_name"`
Assets []asset `json:"assets"`
}

func (r *release) Version() string {
return strings.TrimPrefix(r.TagName, "v")
}
180 changes: 180 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package cli

import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"runtime"
"strings"
"time"

"github.com/blang/semver"
"github.com/exercism/cli/debug"
update "github.com/inconshreveable/go-update"
)

var (
// BuildOS is the operating system (GOOS) used during the build process.
BuildOS string
// BuildARM is the ARM version (GOARM) used during the build process.
BuildARM string
// BuildARCH is the architecture (GOARCH) used during the build process.
BuildARCH string
)

var (
osMap = map[string]string{
"darwin": "mac",
"linux": "linux",
"windows": "windows",
}

archMap = map[string]string{
"amd64": "64bit",
"386": "32bit",
"arm": "arm",
}
)

var (
// HTTPClient is the client used to make HTTP calls in the cli package.
HTTPClient = &http.Client{Timeout: 10 * time.Second}
// LatestReleaseURL is the endpoint that provides information about the latest release.
LatestReleaseURL = "https://api.github.com/repos/exercism/cli/releases/latest"
)

// CLI is information about the CLI itself.
type CLI struct {
Version string
LatestRelease *Release
}

// New creates a CLI, setting it to a particular version.
func New(version string) *CLI {
return &CLI{
Version: version,
}
}

// IsUpToDate compares the current version to that of the latest release.
func (c *CLI) IsUpToDate() (bool, error) {
if c.LatestRelease == nil {
resp, err := HTTPClient.Get(LatestReleaseURL)
if err != nil {
return false, err
}
defer resp.Body.Close()

var rel Release
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return false, err
}
c.LatestRelease = &rel
}

rv, err := semver.Make(c.LatestRelease.Version())
if err != nil {
return false, fmt.Errorf("unable to parse latest version (%s): %s", c.LatestRelease.Version(), err)
}
cv, err := semver.Make(c.Version)
if err != nil {
return false, fmt.Errorf("unable to parse current version (%s): %s", c.Version, err)
}

return rv.EQ(cv), nil
}

// Upgrade allows the user to upgrade to the latest version of the CLI.
func (c *CLI) Upgrade() error {
var (
OS = osMap[runtime.GOOS]
ARCH = archMap[runtime.GOARCH]
)

if OS == "" || ARCH == "" {
return fmt.Errorf("unable to upgrade: OS %s ARCH %s", OS, ARCH)
}

buildName := fmt.Sprintf("%s-%s", OS, ARCH)
if BuildARCH == "arm" {
if BuildARM == "" {
return fmt.Errorf("unable to upgrade: arm version not found")
}
buildName = fmt.Sprintf("%s-v%s", buildName, BuildARM)
}

var downloadRC *bytes.Reader
for _, a := range c.LatestRelease.Assets {
if strings.Contains(a.Name, buildName) {
debug.Printf("Downloading %s\n", a.Name)
var err error
downloadRC, err = a.download()
if err != nil {
return fmt.Errorf("error downloading executable: %s", err)
}
break
}
}
if downloadRC == nil {
return fmt.Errorf("no executable found for %s/%s%s", BuildOS, BuildARCH, BuildARM)
}

bin, err := extractBinary(downloadRC, OS)
if err != nil {
return err
}
defer bin.Close()

return update.Apply(bin, update.Options{})
}

func extractBinary(source *bytes.Reader, os string) (binary io.ReadCloser, err error) {
if os == "windows" {
zr, err := zip.NewReader(source, int64(source.Len()))
if err != nil {
return nil, err
}

for _, f := range zr.File {
return f.Open()
}
} else {
gr, err := gzip.NewReader(source)
if err != nil {
return nil, err
}
defer gr.Close()

tr := tar.NewReader(gr)
for {
_, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
tmpfile, err := ioutil.TempFile("", "temp-exercism")
if err != nil {
return nil, err
}

if _, err = io.Copy(tmpfile, tr); err != nil {
return nil, err
}
if _, err := tmpfile.Seek(0, 0); err != nil {
return nil, err
}

binary = tmpfile
}
}

return binary, nil
}
50 changes: 50 additions & 0 deletions cli/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cli

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsUpToDate(t *testing.T) {
tests := []struct {
cliVersion string
releaseTag string
ok bool
}{
{"1.0.0", "v1.0.1", false},
{"2.0.1", "v2.0.1", true},
}

for _, test := range tests {
c := &CLI{
Version: test.cliVersion,
LatestRelease: &Release{TagName: test.releaseTag},
}

ok, err := c.IsUpToDate()
assert.NoError(t, err)
assert.Equal(t, test.ok, ok, test.cliVersion)
}
}

func TestIsUpToDateWithoutRelease(t *testing.T) {
fakeEndpoint := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"tag_name": "v2.0.0"}`)
})
ts := httptest.NewServer(fakeEndpoint)
defer ts.Close()
LatestReleaseURL = ts.URL

c := &CLI{
Version: "1.0.0",
}

ok, err := c.IsUpToDate()
assert.NoError(t, err)
assert.False(t, ok)
assert.NotNil(t, c.LatestRelease)
}
15 changes: 15 additions & 0 deletions cli/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cli

import "strings"

// Release is a specific build of the CLI, released on GitHub.
type Release struct {
Location string `json:"html_url"`
TagName string `json:"tag_name"`
Assets []Asset `json:"assets"`
}

// Version is the CLI version that is built for the release.
func (r *Release) Version() string {
return strings.TrimPrefix(r.TagName, "v")
}
4 changes: 2 additions & 2 deletions cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"os"

"github.com/exercism/cli/config"
"github.com/urfave/cli"
app "github.com/urfave/cli"
)

// Configure stores settings in a JSON file.
// If a setting is not passed as an argument, default
// values are used.
func Configure(ctx *cli.Context) error {
func Configure(ctx *app.Context) error {
c, err := config.New(ctx.GlobalString("config"))
if err != nil {
log.Fatal(err)
Expand Down
27 changes: 13 additions & 14 deletions cmd/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
"sync"
"time"

"github.com/exercism/cli/cli"
"github.com/exercism/cli/config"
"github.com/exercism/cli/paths"
"github.com/urfave/cli"
app "github.com/urfave/cli"
)

type pingResult struct {
Expand All @@ -23,32 +24,30 @@ type pingResult struct {
}

// Debug provides information about the user's environment and configuration.
func Debug(ctx *cli.Context) error {
func Debug(ctx *app.Context) error {
defer fmt.Printf("\nIf you are having trouble and need to file a GitHub issue (https://github.com/exercism/exercism.io/issues) please include this information (except your API key. Keep that private).\n")

client := &http.Client{Timeout: 20 * time.Second}
cli.HTTPClient = client

fmt.Printf("\n**** Debug Information ****\n")
fmt.Printf("Exercism CLI Version: %s\n", ctx.App.Version)

u, err := NewUpgrader(client)
self := cli.New(ctx.App.Version)
ok, err := self.IsUpToDate()
if err != nil {
log.Println("unable to fetch latest release: " + err.Error())
} else {
rel := u.release
needed, err := u.IsUpgradeNeeded(ctx.App.Version)
if err != nil {
log.Printf("unable to check semver: %s\n", err)
} else if needed {
defer fmt.Printf("\nA newer version of the CLI (%s) can be downloaded here: %s\n", rel.TagName, rel.Location)
if !ok {
defer fmt.Printf("\nA newer version of the CLI (%s) can be downloaded here: %s\n", self.LatestRelease.TagName, self.LatestRelease.Location)
}
fmt.Printf("Exercism CLI Latest Release: %s\n", rel.Version())
fmt.Printf("Exercism CLI Latest Release: %s\n", self.LatestRelease.Version())
}

fmt.Printf("OS/Architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH)
fmt.Printf("Build OS/Architecture %s/%s\n", BuildOS, BuildARCH)
if BuildARM != "" {
fmt.Printf("Build ARMv%s\n", BuildARM)
fmt.Printf("Build OS/Architecture %s/%s\n", cli.BuildOS, cli.BuildARCH)
if cli.BuildARM != "" {
fmt.Printf("Build ARMv%s\n", cli.BuildARM)
}

fmt.Printf("Home Dir: %s\n", paths.Home)
Expand Down Expand Up @@ -119,7 +118,7 @@ func Debug(ctx *cli.Context) error {
return nil
}

func printConfigFileData(ctx *cli.Context, cfg *config.Config) error {
func printConfigFileData(ctx *app.Context, cfg *config.Config) error {
configured := true
if _, err := os.Stat(cfg.File); err != nil {
if os.IsNotExist(err) {
Expand Down
4 changes: 2 additions & 2 deletions cmd/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (

"github.com/exercism/cli/api"
"github.com/exercism/cli/config"
"github.com/urfave/cli"
app "github.com/urfave/cli"
)

// Download returns specified iteration with its related problem.
func Download(ctx *cli.Context) error {
func Download(ctx *app.Context) error {
c, err := config.New(ctx.GlobalString("config"))
if err != nil {
log.Fatal(err)
Expand Down
Loading

0 comments on commit 54de9de

Please sign in to comment.