commit
96aed38819
8 changed files with 512 additions and 49 deletions
|
|
@ -101,16 +101,12 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protocol, _ := cfg.Get(repo.RepoHost(), "git_protocol")
|
||||
return m.Install(ghrepo.FormatRemoteURL(repo, protocol), io.Out, io.ErrOut)
|
||||
return m.Install(repo)
|
||||
},
|
||||
},
|
||||
func() *cobra.Command {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ package extension
|
|||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -39,13 +42,13 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
em.ListFunc = func(bool) []extensions.Extension {
|
||||
return []extensions.Extension{}
|
||||
}
|
||||
em.InstallFunc = func(s string, out, errOut io.Writer) error {
|
||||
em.InstallFunc = func(_ ghrepo.Interface) error {
|
||||
return nil
|
||||
}
|
||||
return func(t *testing.T) {
|
||||
installCalls := em.InstallCalls()
|
||||
assert.Equal(t, 1, len(installCalls))
|
||||
assert.Equal(t, "https://github.com/owner/gh-some-ext.git", installCalls[0].URL)
|
||||
assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName())
|
||||
listCalls := em.ListCalls()
|
||||
assert.Equal(t, 1, len(listCalls))
|
||||
}
|
||||
|
|
@ -281,12 +284,19 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
assertFunc = tt.managerStubs(em)
|
||||
}
|
||||
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
client := http.Client{Transport: ®}
|
||||
|
||||
f := cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
IOStreams: ios,
|
||||
ExtensionManager: em,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &client, nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdExtension(&f)
|
||||
|
|
|
|||
114
pkg/cmd/extension/http.go
Normal file
114
pkg/cmd/extension/http.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
func hasScript(httpClient *http.Client, repo ghrepo.Interface) (hs bool, err error) {
|
||||
path := fmt.Sprintf("repos/%s/%s/contents/%s",
|
||||
repo.RepoOwner(), repo.RepoName(), repo.RepoName())
|
||||
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
err = api.HandleHTTPError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
hs = true
|
||||
return
|
||||
}
|
||||
|
||||
type releaseAsset struct {
|
||||
Name string
|
||||
APIURL string `json:"url"`
|
||||
}
|
||||
|
||||
type release struct {
|
||||
Tag string `json:"tag_name"`
|
||||
Assets []releaseAsset
|
||||
}
|
||||
|
||||
// downloadAsset downloads a single asset to the given file path.
|
||||
func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string) error {
|
||||
req, err := http.NewRequest("GET", asset.APIURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/octet-stream")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// fetchLatestRelease finds the latest published release for a repository.
|
||||
func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*release, error) {
|
||||
path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r release
|
||||
err = json.Unmarshal(b, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
|
@ -14,10 +15,14 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/findsh"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/safeexec"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
|
|
@ -25,17 +30,32 @@ type Manager struct {
|
|||
lookPath func(string) (string, error)
|
||||
findSh func() (string, error)
|
||||
newCommand func(string, ...string) *exec.Cmd
|
||||
platform func() string
|
||||
client *http.Client
|
||||
config config.Config
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
func NewManager(io *iostreams.IOStreams) *Manager {
|
||||
return &Manager{
|
||||
dataDir: config.DataDir,
|
||||
lookPath: safeexec.LookPath,
|
||||
findSh: findsh.Find,
|
||||
newCommand: exec.Command,
|
||||
platform: func() string {
|
||||
return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SetConfig(cfg config.Config) {
|
||||
m.config = cfg
|
||||
}
|
||||
|
||||
func (m *Manager) SetClient(client *http.Client) {
|
||||
m.client = client
|
||||
}
|
||||
|
||||
func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) {
|
||||
if len(args) == 0 {
|
||||
return false, errors.New("too few arguments in list")
|
||||
|
|
@ -172,7 +192,104 @@ func (m *Manager) InstallLocal(dir string) error {
|
|||
return makeSymlink(dir, targetLink)
|
||||
}
|
||||
|
||||
func (m *Manager) Install(cloneURL string, stdout, stderr io.Writer) error {
|
||||
type binManifest struct {
|
||||
Owner string
|
||||
Name string
|
||||
Host string
|
||||
Tag string
|
||||
// TODO I may end up not using this; just thinking ahead to local installs
|
||||
Path string
|
||||
}
|
||||
|
||||
func (m *Manager) Install(repo ghrepo.Interface) error {
|
||||
isBin, err := isBinExtension(m.client, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not check for binary extension: %w", err)
|
||||
}
|
||||
if isBin {
|
||||
return m.installBin(repo)
|
||||
}
|
||||
|
||||
hs, err := hasScript(m.client, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hs {
|
||||
// TODO open an issue hint, here?
|
||||
return errors.New("extension is uninstallable: missing executable")
|
||||
}
|
||||
|
||||
protocol, _ := m.config.Get(repo.RepoHost(), "git_protocol")
|
||||
return m.installGit(ghrepo.FormatRemoteURL(repo, protocol), m.io.Out, m.io.ErrOut)
|
||||
}
|
||||
|
||||
func (m *Manager) installBin(repo ghrepo.Interface) error {
|
||||
var r *release
|
||||
r, err := fetchLatestRelease(m.client, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
suffix := m.platform()
|
||||
var asset *releaseAsset
|
||||
for _, a := range r.Assets {
|
||||
if strings.HasSuffix(a.Name, suffix) {
|
||||
asset = &a
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if asset == nil {
|
||||
return fmt.Errorf("%s unsupported for %s. Open an issue: `gh issue create -R %s/%s -t'Support %s'`",
|
||||
repo.RepoName(),
|
||||
suffix, repo.RepoOwner(), repo.RepoName(), suffix)
|
||||
}
|
||||
|
||||
name := repo.RepoName()
|
||||
targetDir := filepath.Join(m.installDir(), name)
|
||||
// TODO clean this up if function errs?
|
||||
err = os.MkdirAll(targetDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create installation directory: %w", err)
|
||||
}
|
||||
|
||||
binPath := filepath.Join(targetDir, name)
|
||||
|
||||
err = downloadAsset(m.client, *asset, binPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download asset %s: %w", asset.Name, err)
|
||||
}
|
||||
|
||||
manifest := binManifest{
|
||||
Name: name,
|
||||
Owner: repo.RepoOwner(),
|
||||
Host: repo.RepoHost(),
|
||||
Path: binPath,
|
||||
Tag: r.Tag,
|
||||
}
|
||||
|
||||
bs, err := yaml.Marshal(manifest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize manifest: %w", err)
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(targetDir, "manifest.yml")
|
||||
|
||||
f, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open manifest for writing: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(bs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed write manifest file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) installGit(cloneURL string, stdout, stderr io.Writer) error {
|
||||
exe, err := m.lookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -346,3 +463,77 @@ func readPathFromFile(path string) (string, error) {
|
|||
n, err := f.Read(b)
|
||||
return strings.TrimSpace(string(b[:n])), err
|
||||
}
|
||||
|
||||
func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err error) {
|
||||
var r *release
|
||||
r, err = fetchLatestRelease(client, repo)
|
||||
if err != nil {
|
||||
httpErr, ok := err.(api.HTTPError)
|
||||
if ok && httpErr.StatusCode == 404 {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, a := range r.Assets {
|
||||
dists := possibleDists()
|
||||
for _, d := range dists {
|
||||
if strings.HasSuffix(a.Name, d) {
|
||||
isBin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func possibleDists() []string {
|
||||
return []string{
|
||||
"aix-ppc64",
|
||||
"android-386",
|
||||
"android-amd64",
|
||||
"android-arm",
|
||||
"android-arm64",
|
||||
"darwin-amd64",
|
||||
"darwin-arm64",
|
||||
"dragonfly-amd64",
|
||||
"freebsd-386",
|
||||
"freebsd-amd64",
|
||||
"freebsd-arm",
|
||||
"freebsd-arm64",
|
||||
"illumos-amd64",
|
||||
"ios-amd64",
|
||||
"ios-arm64",
|
||||
"js-wasm",
|
||||
"linux-386",
|
||||
"linux-amd64",
|
||||
"linux-arm",
|
||||
"linux-arm64",
|
||||
"linux-mips",
|
||||
"linux-mips64",
|
||||
"linux-mips64le",
|
||||
"linux-mipsle",
|
||||
"linux-ppc64",
|
||||
"linux-ppc64le",
|
||||
"linux-riscv64",
|
||||
"linux-s390x",
|
||||
"netbsd-386",
|
||||
"netbsd-amd64",
|
||||
"netbsd-arm",
|
||||
"netbsd-arm64",
|
||||
"openbsd-386",
|
||||
"openbsd-amd64",
|
||||
"openbsd-arm",
|
||||
"openbsd-arm64",
|
||||
"openbsd-mips64",
|
||||
"plan9-386",
|
||||
"plan9-amd64",
|
||||
"plan9-arm",
|
||||
"solaris-amd64",
|
||||
"windows-386",
|
||||
"windows-amd64",
|
||||
"windows-arm",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -11,7 +12,12 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestHelperProcess(t *testing.T) {
|
||||
|
|
@ -28,7 +34,7 @@ func TestHelperProcess(t *testing.T) {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
func newTestManager(dir string) *Manager {
|
||||
func newTestManager(dir string, client *http.Client, io *iostreams.IOStreams) *Manager {
|
||||
return &Manager{
|
||||
dataDir: func() string { return dir },
|
||||
lookPath: func(exe string) (string, error) { return exe, nil },
|
||||
|
|
@ -39,6 +45,12 @@ func newTestManager(dir string) *Manager {
|
|||
cmd.Env = []string{"GH_WANT_HELPER_PROCESS=1"}
|
||||
return cmd
|
||||
},
|
||||
config: config.NewBlankConfig(),
|
||||
io: io,
|
||||
client: client,
|
||||
platform: func() string {
|
||||
return "windows-amd64"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +59,7 @@ func TestManager_List(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
|
||||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two")))
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
exts := m.List(false)
|
||||
assert.Equal(t, 2, len(exts))
|
||||
assert.Equal(t, "hello", exts[0].Name())
|
||||
|
|
@ -59,7 +71,7 @@ func TestManager_Dispatch(t *testing.T) {
|
|||
extPath := filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")
|
||||
assert.NoError(t, stubExtension(extPath))
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
|
@ -80,7 +92,7 @@ func TestManager_Remove(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
|
||||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two")))
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
err := m.Remove("hello")
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -96,7 +108,7 @@ func TestManager_Upgrade_AllExtensions(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two")))
|
||||
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
|
@ -121,7 +133,7 @@ func TestManager_Upgrade_RemoteExtension(t *testing.T) {
|
|||
tempDir := t.TempDir()
|
||||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
|
@ -141,7 +153,7 @@ func TestManager_Upgrade_LocalExtension(t *testing.T) {
|
|||
tempDir := t.TempDir()
|
||||
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
|
@ -158,7 +170,7 @@ func TestManager_Upgrade_Force(t *testing.T) {
|
|||
|
||||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
|
@ -180,7 +192,7 @@ func TestManager_Upgrade_Force(t *testing.T) {
|
|||
func TestManager_Upgrade_NoExtensions(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
|
|
@ -190,24 +202,153 @@ func TestManager_Upgrade_NoExtensions(t *testing.T) {
|
|||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
func TestManager_Install(t *testing.T) {
|
||||
func TestManager_Install_git(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
m := newTestManager(tempDir)
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
err := m.Install("https://github.com/owner/gh-some-ext.git", stdout, stderr)
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
client := http.Client{Transport: ®}
|
||||
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
m := newTestManager(tempDir, &client, io)
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/gh-some-ext/releases/latest"),
|
||||
httpmock.JSONResponse(
|
||||
release{
|
||||
Assets: []releaseAsset{
|
||||
{
|
||||
Name: "not-a-binary",
|
||||
APIURL: "https://example.com/release/cool",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/gh-some-ext/contents/gh-some-ext"),
|
||||
httpmock.StringResponse("script"))
|
||||
|
||||
repo := ghrepo.New("owner", "gh-some-ext")
|
||||
|
||||
err := m.Install(repo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fmt.Sprintf("[git clone https://github.com/owner/gh-some-ext.git %s]\n", filepath.Join(tempDir, "extensions", "gh-some-ext")), stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
func TestManager_Install_binary_unsupported(t *testing.T) {
|
||||
repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com")
|
||||
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
client := http.Client{Transport: ®}
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
|
||||
httpmock.JSONResponse(
|
||||
release{
|
||||
Assets: []releaseAsset{
|
||||
{
|
||||
Name: "gh-bin-ext-linux-amd64",
|
||||
APIURL: "https://example.com/release/cool",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
|
||||
httpmock.JSONResponse(
|
||||
release{
|
||||
Tag: "v1.0.1",
|
||||
Assets: []releaseAsset{
|
||||
{
|
||||
Name: "gh-bin-ext-linux-amd64",
|
||||
APIURL: "https://example.com/release/cool",
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
m := newTestManager(tempDir, &client, io)
|
||||
|
||||
err := m.Install(repo)
|
||||
assert.Error(t, err)
|
||||
|
||||
errText := "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`"
|
||||
|
||||
assert.Equal(t, errText, err.Error())
|
||||
}
|
||||
|
||||
func TestManager_Install_binary(t *testing.T) {
|
||||
repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com")
|
||||
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
client := http.Client{Transport: ®}
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
|
||||
httpmock.JSONResponse(
|
||||
release{
|
||||
Assets: []releaseAsset{
|
||||
{
|
||||
Name: "gh-bin-ext-windows-amd64",
|
||||
APIURL: "https://example.com/release/cool",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
|
||||
httpmock.JSONResponse(
|
||||
release{
|
||||
Tag: "v1.0.1",
|
||||
Assets: []releaseAsset{
|
||||
{
|
||||
Name: "gh-bin-ext-windows-amd64",
|
||||
APIURL: "https://example.com/release/cool",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "release/cool"),
|
||||
httpmock.StringResponse("FAKE BINARY"))
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
m := newTestManager(tempDir, &client, io)
|
||||
|
||||
err := m.Install(repo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/manifest.yml"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var bm binManifest
|
||||
err = yaml.Unmarshal(manifest, &bm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, binManifest{
|
||||
Name: "gh-bin-ext",
|
||||
Owner: "owner",
|
||||
Host: "example.com",
|
||||
Tag: "v1.0.1",
|
||||
Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"),
|
||||
}, bm)
|
||||
|
||||
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "FAKE BINARY", string(fakeBin))
|
||||
}
|
||||
|
||||
func TestManager_Create(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
oldWd, _ := os.Getwd()
|
||||
assert.NoError(t, os.Chdir(tempDir))
|
||||
t.Cleanup(func() { _ = os.Chdir(oldWd) })
|
||||
m := newTestManager(tempDir)
|
||||
m := newTestManager(tempDir, nil, nil)
|
||||
err := m.Create("gh-test")
|
||||
assert.NoError(t, err)
|
||||
files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test"))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/context"
|
||||
|
|
@ -22,7 +23,6 @@ func New(appVersion string) *cmdutil.Factory {
|
|||
Branch: branchFunc(), // No factory dependencies
|
||||
Executable: executable(), // No factory dependencies
|
||||
|
||||
ExtensionManager: extension.NewManager(),
|
||||
}
|
||||
|
||||
f.IOStreams = ioStreams(f) // Depends on Config
|
||||
|
|
@ -30,6 +30,7 @@ func New(appVersion string) *cmdutil.Factory {
|
|||
f.Remotes = remotesFunc(f) // Depends on Config
|
||||
f.BaseRepo = BaseRepoFunc(f) // Depends on Remotes
|
||||
f.Browser = browser(f) // Depends on Config, and IOStreams
|
||||
f.ExtensionManager = extensionManager(f) // Depends on Config, HttpClient, and IOStreams
|
||||
|
||||
return f
|
||||
}
|
||||
|
|
@ -148,6 +149,25 @@ func branchFunc() func() (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func extensionManager(f *cmdutil.Factory) *extension.Manager {
|
||||
em := extension.NewManager(f.IOStreams)
|
||||
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return em
|
||||
}
|
||||
em.SetConfig(cfg)
|
||||
|
||||
client, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return em
|
||||
}
|
||||
|
||||
em.SetClient(api.NewCachedClient(client, time.Second*30))
|
||||
|
||||
return em
|
||||
}
|
||||
|
||||
func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
|
||||
io := iostreams.System()
|
||||
cfg, err := f.Config()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package extensions
|
|||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
//go:generate moq -rm -out extension_mock.go . Extension
|
||||
|
|
@ -16,7 +18,7 @@ type Extension interface {
|
|||
//go:generate moq -rm -out manager_mock.go . ExtensionManager
|
||||
type ExtensionManager interface {
|
||||
List(includeMetadata bool) []Extension
|
||||
Install(url string, stdout, stderr io.Writer) error
|
||||
Install(ghrepo.Interface) error
|
||||
InstallLocal(dir string) error
|
||||
Upgrade(name string, force bool, stdout, stderr io.Writer) error
|
||||
Remove(name string) error
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
|
@ -24,7 +25,7 @@ var _ ExtensionManager = &ExtensionManagerMock{}
|
|||
// DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) {
|
||||
// panic("mock out the Dispatch method")
|
||||
// },
|
||||
// InstallFunc: func(url string, stdout io.Writer, stderr io.Writer) error {
|
||||
// InstallFunc: func(interfaceMoqParam ghrepo.Interface) error {
|
||||
// panic("mock out the Install method")
|
||||
// },
|
||||
// InstallLocalFunc: func(dir string) error {
|
||||
|
|
@ -53,7 +54,7 @@ type ExtensionManagerMock struct {
|
|||
DispatchFunc func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error)
|
||||
|
||||
// InstallFunc mocks the Install method.
|
||||
InstallFunc func(url string, stdout io.Writer, stderr io.Writer) error
|
||||
InstallFunc func(interfaceMoqParam ghrepo.Interface) error
|
||||
|
||||
// InstallLocalFunc mocks the InstallLocal method.
|
||||
InstallLocalFunc func(dir string) error
|
||||
|
|
@ -87,12 +88,8 @@ type ExtensionManagerMock struct {
|
|||
}
|
||||
// Install holds details about calls to the Install method.
|
||||
Install []struct {
|
||||
// URL is the url argument value.
|
||||
URL string
|
||||
// Stdout is the stdout argument value.
|
||||
Stdout io.Writer
|
||||
// Stderr is the stderr argument value.
|
||||
Stderr io.Writer
|
||||
// InterfaceMoqParam is the interfaceMoqParam argument value.
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
}
|
||||
// InstallLocal holds details about calls to the InstallLocal method.
|
||||
InstallLocal []struct {
|
||||
|
|
@ -205,37 +202,29 @@ func (mock *ExtensionManagerMock) DispatchCalls() []struct {
|
|||
}
|
||||
|
||||
// Install calls InstallFunc.
|
||||
func (mock *ExtensionManagerMock) Install(url string, stdout io.Writer, stderr io.Writer) error {
|
||||
func (mock *ExtensionManagerMock) Install(interfaceMoqParam ghrepo.Interface) error {
|
||||
if mock.InstallFunc == nil {
|
||||
panic("ExtensionManagerMock.InstallFunc: method is nil but ExtensionManager.Install was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
URL string
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
}{
|
||||
URL: url,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
InterfaceMoqParam: interfaceMoqParam,
|
||||
}
|
||||
mock.lockInstall.Lock()
|
||||
mock.calls.Install = append(mock.calls.Install, callInfo)
|
||||
mock.lockInstall.Unlock()
|
||||
return mock.InstallFunc(url, stdout, stderr)
|
||||
return mock.InstallFunc(interfaceMoqParam)
|
||||
}
|
||||
|
||||
// InstallCalls gets all the calls that were made to Install.
|
||||
// Check the length with:
|
||||
// len(mockedExtensionManager.InstallCalls())
|
||||
func (mock *ExtensionManagerMock) InstallCalls() []struct {
|
||||
URL string
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
} {
|
||||
var calls []struct {
|
||||
URL string
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
}
|
||||
mock.lockInstall.RLock()
|
||||
calls = mock.calls.Install
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue