-
-
Notifications
You must be signed in to change notification settings - Fork 361
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #407 from exercism/refactor-upgrade
Refactor upgrade command
- Loading branch information
Showing
16 changed files
with
292 additions
and
245 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.