Merge pull request #5272 from meiji163/pin-ext
This commit is contained in:
commit
c1e5934b21
9 changed files with 466 additions and 61 deletions
|
|
@ -65,7 +65,13 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
if !c.IsBinary() && len(version) > 8 {
|
||||
version = version[:8]
|
||||
}
|
||||
t.AddField(version, nil, nil)
|
||||
|
||||
if c.IsPinned() {
|
||||
t.AddField(version, nil, cs.Cyan)
|
||||
} else {
|
||||
t.AddField(version, nil, nil)
|
||||
}
|
||||
|
||||
var updateAvailable string
|
||||
if c.UpdateAvailable() {
|
||||
updateAvailable = "Upgrade available"
|
||||
|
|
@ -76,10 +82,12 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
return t.Render()
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "install <repository>",
|
||||
Short: "Install a gh extension from a repository",
|
||||
Long: heredoc.Doc(`
|
||||
func() *cobra.Command {
|
||||
var pinFlag string
|
||||
cmd := &cobra.Command{
|
||||
Use: "install <repository>",
|
||||
Short: "Install a gh extension from a repository",
|
||||
Long: heredoc.Doc(`
|
||||
Install a GitHub repository locally as a GitHub CLI extension.
|
||||
|
||||
The repository argument can be specified in "owner/repo" format as well as a full URL.
|
||||
|
|
@ -90,41 +98,57 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
|
||||
See the list of available extensions at <https://github.com/topics/gh-extension>.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
Example: heredoc.Doc(`
|
||||
$ gh extension install owner/gh-extension
|
||||
$ gh extension install https://git.example.com/owner/gh-extension
|
||||
$ gh extension install .
|
||||
`),
|
||||
Args: cmdutil.MinimumArgs(1, "must specify a repository to install from"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "." {
|
||||
wd, err := os.Getwd()
|
||||
Args: cmdutil.MinimumArgs(1, "must specify a repository to install from"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "." {
|
||||
if pinFlag != "" {
|
||||
return fmt.Errorf("local extensions cannot be pinned")
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.InstallLocal(wd)
|
||||
}
|
||||
|
||||
repo, err := ghrepo.FromFullName(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.InstallLocal(wd)
|
||||
}
|
||||
|
||||
repo, err := ghrepo.FromFullName(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.Install(repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if io.IsStdoutTTY() {
|
||||
cs := io.ColorScheme()
|
||||
fmt.Fprintf(io.Out, "%s Installed extension %s\n", cs.SuccessIcon(), args[0])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
if err := m.Install(repo, pinFlag); err != nil {
|
||||
if errors.Is(err, releaseNotFoundErr) {
|
||||
return fmt.Errorf("%s Could not find a release of %s for %s",
|
||||
cs.FailureIcon(), args[0], cs.Cyan(pinFlag))
|
||||
} else if errors.Is(err, commitNotFoundErr) {
|
||||
return fmt.Errorf("%s %s does not exist in %s",
|
||||
cs.FailureIcon(), cs.Cyan(pinFlag), args[0])
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if io.IsStdoutTTY() {
|
||||
fmt.Fprintf(io.Out, "%s Installed extension %s\n", cs.SuccessIcon(), args[0])
|
||||
if pinFlag != "" {
|
||||
fmt.Fprintf(io.Out, "%s Pinned extension at %s\n", cs.SuccessIcon(), cs.Cyan(pinFlag))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&pinFlag, "pin", "", "pin extension to a release tag or commit ref")
|
||||
return cmd
|
||||
}(),
|
||||
func() *cobra.Command {
|
||||
var flagAll bool
|
||||
var flagForce bool
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
em.ListFunc = func(bool) []extensions.Extension {
|
||||
return []extensions.Extension{}
|
||||
}
|
||||
em.InstallFunc = func(_ ghrepo.Interface) error {
|
||||
em.InstallFunc = func(_ ghrepo.Interface, _ string) error {
|
||||
return nil
|
||||
}
|
||||
return func(t *testing.T) {
|
||||
|
|
@ -86,6 +86,13 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "install local extension with pin",
|
||||
args: []string{"install", ".", "--pin", "v1.0.0"},
|
||||
wantErr: true,
|
||||
errMsg: "local extensions cannot be pinned",
|
||||
isTTY: true,
|
||||
},
|
||||
{
|
||||
name: "upgrade argument error",
|
||||
args: []string{"upgrade"},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ type Extension struct {
|
|||
path string
|
||||
url string
|
||||
isLocal bool
|
||||
isPinned bool
|
||||
currentVersion string
|
||||
latestVersion string
|
||||
kind ExtensionKind
|
||||
|
|
@ -43,8 +44,13 @@ func (e *Extension) CurrentVersion() string {
|
|||
return e.currentVersion
|
||||
}
|
||||
|
||||
func (e *Extension) IsPinned() bool {
|
||||
return e.isPinned
|
||||
}
|
||||
|
||||
func (e *Extension) UpdateAvailable() bool {
|
||||
if e.isLocal ||
|
||||
if e.isPinned ||
|
||||
e.isLocal ||
|
||||
e.currentVersion == "" ||
|
||||
e.latestVersion == "" ||
|
||||
e.currentVersion == e.latestVersion {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package extension
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
|
@ -80,6 +81,9 @@ func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string)
|
|||
return err
|
||||
}
|
||||
|
||||
var releaseNotFoundErr = errors.New("release not found")
|
||||
var commitNotFoundErr = errors.New("commit not found")
|
||||
|
||||
// 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())
|
||||
|
|
@ -112,3 +116,71 @@ func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*re
|
|||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// fetchReleaseFromTag finds release by tag name for a repository
|
||||
func fetchReleaseFromTag(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*release, error) {
|
||||
fullRepoName := fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
path := fmt.Sprintf("repos/%s/releases/tags/%s", fullRepoName, tagName)
|
||||
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 == 404 {
|
||||
return nil, releaseNotFoundErr
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// fetchCommitSHA finds full commit SHA from a target ref in a repo
|
||||
func fetchCommitSHA(httpClient *http.Client, baseRepo ghrepo.Interface, targetRef string) (string, error) {
|
||||
path := fmt.Sprintf("repos/%s/%s/commits/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), targetRef)
|
||||
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github.VERSION.sha")
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == 422 {
|
||||
return "", commitNotFoundErr
|
||||
}
|
||||
if resp.StatusCode > 299 {
|
||||
return "", api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ func (m *Manager) parseBinaryExtensionDir(fi fs.FileInfo) (Extension, error) {
|
|||
remoteURL := ghrepo.GenerateRepoURL(repo, "")
|
||||
ext.url = remoteURL
|
||||
ext.currentVersion = bm.Tag
|
||||
ext.isPinned = bm.IsPinned
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
|
|
@ -206,12 +207,20 @@ func (m *Manager) parseGitExtensionDir(fi fs.FileInfo) (Extension, error) {
|
|||
exePath := filepath.Join(id, fi.Name(), fi.Name())
|
||||
remoteUrl := m.getRemoteUrl(fi.Name())
|
||||
currentVersion := m.getCurrentVersion(fi.Name())
|
||||
|
||||
var isPinned bool
|
||||
pinPath := filepath.Join(id, fi.Name(), fmt.Sprintf(".pin-%s", currentVersion))
|
||||
if _, err := os.Stat(pinPath); err == nil {
|
||||
isPinned = true
|
||||
}
|
||||
|
||||
return Extension{
|
||||
path: exePath,
|
||||
url: remoteUrl,
|
||||
isLocal: false,
|
||||
currentVersion: currentVersion,
|
||||
kind: GitKind,
|
||||
isPinned: isPinned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -224,6 +233,7 @@ func (m *Manager) getCurrentVersion(extension string) string {
|
|||
dir := m.installDir()
|
||||
gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git")
|
||||
cmd := m.newCommand(gitExe, gitDir, "rev-parse", "HEAD")
|
||||
|
||||
localSha, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
|
|
@ -312,21 +322,23 @@ func (m *Manager) InstallLocal(dir string) error {
|
|||
}
|
||||
|
||||
type binManifest struct {
|
||||
Owner string
|
||||
Name string
|
||||
Host string
|
||||
Tag string
|
||||
Owner string
|
||||
Name string
|
||||
Host string
|
||||
Tag string
|
||||
IsPinned bool
|
||||
// TODO I may end up not using this; just thinking ahead to local installs
|
||||
Path string
|
||||
}
|
||||
|
||||
func (m *Manager) Install(repo ghrepo.Interface) error {
|
||||
// Install installs an extension from repo, and pins to commitish if provided
|
||||
func (m *Manager) Install(repo ghrepo.Interface, target string) 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)
|
||||
return m.installBin(repo, target)
|
||||
}
|
||||
|
||||
hs, err := hasScript(m.client, repo)
|
||||
|
|
@ -337,13 +349,18 @@ func (m *Manager) Install(repo ghrepo.Interface) error {
|
|||
return errors.New("extension is not installable: missing executable")
|
||||
}
|
||||
|
||||
protocol, _ := m.config.GetOrDefault(repo.RepoHost(), "git_protocol")
|
||||
return m.installGit(ghrepo.FormatRemoteURL(repo, protocol), m.io.Out, m.io.ErrOut)
|
||||
return m.installGit(repo, target, m.io.Out, m.io.ErrOut)
|
||||
}
|
||||
|
||||
func (m *Manager) installBin(repo ghrepo.Interface) error {
|
||||
func (m *Manager) installBin(repo ghrepo.Interface, target string) error {
|
||||
var r *release
|
||||
r, err := fetchLatestRelease(m.client, repo)
|
||||
var err error
|
||||
isPinned := target != ""
|
||||
if isPinned {
|
||||
r, err = fetchReleaseFromTag(m.client, repo, target)
|
||||
} else {
|
||||
r, err = fetchLatestRelease(m.client, repo)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -365,6 +382,7 @@ func (m *Manager) installBin(repo ghrepo.Interface) error {
|
|||
|
||||
name := repo.RepoName()
|
||||
targetDir := filepath.Join(m.installDir(), name)
|
||||
|
||||
// TODO clean this up if function errs?
|
||||
err = os.MkdirAll(targetDir, 0755)
|
||||
if err != nil {
|
||||
|
|
@ -380,11 +398,12 @@ func (m *Manager) installBin(repo ghrepo.Interface) error {
|
|||
}
|
||||
|
||||
manifest := binManifest{
|
||||
Name: name,
|
||||
Owner: repo.RepoOwner(),
|
||||
Host: repo.RepoHost(),
|
||||
Path: binPath,
|
||||
Tag: r.Tag,
|
||||
Name: name,
|
||||
Owner: repo.RepoOwner(),
|
||||
Host: repo.RepoHost(),
|
||||
Path: binPath,
|
||||
Tag: r.Tag,
|
||||
IsPinned: isPinned,
|
||||
}
|
||||
|
||||
bs, err := yaml.Marshal(manifest)
|
||||
|
|
@ -408,21 +427,52 @@ func (m *Manager) installBin(repo ghrepo.Interface) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) installGit(cloneURL string, stdout, stderr io.Writer) error {
|
||||
func (m *Manager) installGit(repo ghrepo.Interface, target string, stdout, stderr io.Writer) error {
|
||||
protocol, _ := m.config.GetOrDefault(repo.RepoHost(), "git_protocol")
|
||||
cloneURL := ghrepo.FormatRemoteURL(repo, protocol)
|
||||
|
||||
exe, err := m.lookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var commitSHA string
|
||||
if target != "" {
|
||||
commitSHA, err = fetchCommitSHA(m.client, repo, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(path.Base(cloneURL), ".git")
|
||||
targetDir := filepath.Join(m.installDir(), name)
|
||||
|
||||
externalCmd := m.newCommand(exe, "clone", cloneURL, targetDir)
|
||||
externalCmd.Stdout = stdout
|
||||
externalCmd.Stderr = stderr
|
||||
return externalCmd.Run()
|
||||
if err := externalCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if commitSHA == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
checkoutCmd := m.newCommand(exe, "-C", targetDir, "checkout", commitSHA)
|
||||
checkoutCmd.Stdout = stdout
|
||||
checkoutCmd.Stderr = stderr
|
||||
if err := checkoutCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pinPath := filepath.Join(targetDir, fmt.Sprintf(".pin-%s", commitSHA))
|
||||
f, err := os.OpenFile(pinPath, os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create pin file in directory: %w", err)
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
var pinnedExtensionUpgradeError = errors.New("pinned extensions can not be upgraded")
|
||||
var localExtensionUpgradeError = errors.New("local extensions can not be upgraded")
|
||||
var upToDateError = errors.New("already up to date")
|
||||
var noExtensionsInstalledError = errors.New("no extensions installed")
|
||||
|
|
@ -462,7 +512,8 @@ func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
|
|||
err := m.upgradeExtension(f, force)
|
||||
if err != nil {
|
||||
if !errors.Is(err, localExtensionUpgradeError) &&
|
||||
!errors.Is(err, upToDateError) {
|
||||
!errors.Is(err, upToDateError) &&
|
||||
!errors.Is(err, pinnedExtensionUpgradeError) {
|
||||
failed = true
|
||||
}
|
||||
fmt.Fprintf(m.io.Out, "%s\n", err)
|
||||
|
|
@ -480,6 +531,9 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
|
|||
if ext.isLocal {
|
||||
return localExtensionUpgradeError
|
||||
}
|
||||
if ext.IsPinned() {
|
||||
return pinnedExtensionUpgradeError
|
||||
}
|
||||
if !ext.UpdateAvailable() {
|
||||
return upToDateError
|
||||
}
|
||||
|
|
@ -498,7 +552,7 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate to new precompiled extension format: %w", err)
|
||||
}
|
||||
return m.installBin(repo)
|
||||
return m.installBin(repo, "")
|
||||
}
|
||||
err = m.upgradeGitExtension(ext, force)
|
||||
}
|
||||
|
|
@ -525,7 +579,7 @@ func (m *Manager) upgradeBinExtension(ext Extension) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to parse URL %s: %w", ext.url, err)
|
||||
}
|
||||
return m.installBin(repo)
|
||||
return m.installBin(repo, "")
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(name string) error {
|
||||
|
|
|
|||
|
|
@ -445,6 +445,50 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
|
|||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
func TestManager_UpgradeExtension_BinaryExtension_Pinned(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
assert.NoError(t, stubBinaryExtension(
|
||||
filepath.Join(tempDir, "extensions", "gh-bin-ext"),
|
||||
binManifest{
|
||||
Owner: "owner",
|
||||
Name: "gh-bin-ext",
|
||||
Host: "example.com",
|
||||
Tag: "v1.6.3",
|
||||
IsPinned: true,
|
||||
}))
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
m := newTestManager(tempDir, nil, io)
|
||||
exts, err := m.list(false)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(exts))
|
||||
ext := exts[0]
|
||||
|
||||
err = m.upgradeExtension(ext, false)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, err, pinnedExtensionUpgradeError)
|
||||
}
|
||||
|
||||
func TestManager_UpgradeExtenion_GitExtension_Pinned(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
extDir := filepath.Join(tempDir, "extensions", "gh-remote")
|
||||
assert.NoError(t, stubPinnedExtension(filepath.Join(extDir, "gh-remote"), "abcd1234"))
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
m := newTestManager(tempDir, nil, io)
|
||||
exts, err := m.list(false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(exts))
|
||||
ext := exts[0]
|
||||
ext.isPinned = true
|
||||
ext.latestVersion = "new version"
|
||||
|
||||
err = m.upgradeExtension(ext, false)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, err, pinnedExtensionUpgradeError)
|
||||
}
|
||||
|
||||
func TestManager_Install_git(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
|
|
@ -472,12 +516,113 @@ func TestManager_Install_git(t *testing.T) {
|
|||
|
||||
repo := ghrepo.New("owner", "gh-some-ext")
|
||||
|
||||
err := m.Install(repo)
|
||||
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_git_pinned(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
client := http.Client{Transport: ®}
|
||||
|
||||
io, _, _, stderr := iostreams.Test()
|
||||
m := newTestManager(tempDir, &client, io)
|
||||
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/gh-cool-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-cool-ext/commits/some-ref"),
|
||||
httpmock.StringResponse("abcd1234"))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/owner/gh-cool-ext/contents/gh-cool-ext"),
|
||||
httpmock.StringResponse("script"))
|
||||
|
||||
_ = os.MkdirAll(filepath.Join(m.installDir(), "gh-cool-ext"), 0700)
|
||||
repo := ghrepo.New("owner", "gh-cool-ext")
|
||||
err := m.Install(repo, "some-ref")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
func TestManager_Install_binary_pinned(t *testing.T) {
|
||||
repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com")
|
||||
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
||||
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.exe",
|
||||
APIURL: "https://example.com/release/cool",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/tags/v1.6.3-pre"),
|
||||
httpmock.JSONResponse(
|
||||
release{
|
||||
Tag: "v1.6.3-pre",
|
||||
Assets: []releaseAsset{
|
||||
{
|
||||
Name: "gh-bin-ext-windows-amd64.exe",
|
||||
APIURL: "https://example.com/release/cool",
|
||||
},
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "release/cool"),
|
||||
httpmock.StringResponse("FAKE BINARY"))
|
||||
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
m := newTestManager(tempDir, &http.Client{Transport: ®}, io)
|
||||
|
||||
err := m.Install(repo, "v1.6.3-pre")
|
||||
assert.NoError(t, err)
|
||||
|
||||
manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName))
|
||||
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.6.3-pre",
|
||||
IsPinned: true,
|
||||
Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"),
|
||||
}, bm)
|
||||
|
||||
fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "FAKE BINARY", string(fakeBin))
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
|
||||
}
|
||||
|
||||
func TestManager_Install_binary_unsupported(t *testing.T) {
|
||||
repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com")
|
||||
|
||||
|
|
@ -514,7 +659,7 @@ func TestManager_Install_binary_unsupported(t *testing.T) {
|
|||
|
||||
m := newTestManager(tempDir, &client, io)
|
||||
|
||||
err := m.Install(repo)
|
||||
err := m.Install(repo, "")
|
||||
assert.EqualError(t, err, "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, "", stdout.String())
|
||||
|
|
@ -559,7 +704,7 @@ func TestManager_Install_binary(t *testing.T) {
|
|||
|
||||
m := newTestManager(tempDir, &http.Client{Transport: ®}, io)
|
||||
|
||||
err := m.Install(repo)
|
||||
err := m.Install(repo, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName))
|
||||
|
|
@ -702,6 +847,24 @@ func stubExtension(path string) error {
|
|||
return f.Close()
|
||||
}
|
||||
|
||||
func stubPinnedExtension(path string, pinnedVersion string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
|
||||
pinPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".pin-%s", pinnedVersion))
|
||||
f, err = os.OpenFile(pinPath, os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func stubLocalExtension(tempDir, path string) error {
|
||||
extDir, err := ioutil.TempDir(tempDir, "local-ext")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -19,16 +19,17 @@ type Extension interface {
|
|||
Name() string // Extension Name without gh-
|
||||
Path() string // Path to executable
|
||||
URL() string
|
||||
IsLocal() bool
|
||||
CurrentVersion() string
|
||||
IsPinned() bool
|
||||
UpdateAvailable() bool
|
||||
IsBinary() bool
|
||||
IsLocal() bool
|
||||
}
|
||||
|
||||
//go:generate moq -rm -out manager_mock.go . ExtensionManager
|
||||
type ExtensionManager interface {
|
||||
List(includeMetadata bool) []Extension
|
||||
Install(ghrepo.Interface) error
|
||||
Install(ghrepo.Interface, string) error
|
||||
InstallLocal(dir string) error
|
||||
Upgrade(name string, force bool) error
|
||||
Remove(name string) error
|
||||
|
|
|
|||
|
|
@ -26,12 +26,18 @@ var _ Extension = &ExtensionMock{}
|
|||
// IsLocalFunc: func() bool {
|
||||
// panic("mock out the IsLocal method")
|
||||
// },
|
||||
// IsPinnedFunc: func() bool {
|
||||
// panic("mock out the IsPinned method")
|
||||
// },
|
||||
// NameFunc: func() string {
|
||||
// panic("mock out the Name method")
|
||||
// },
|
||||
// PathFunc: func() string {
|
||||
// panic("mock out the Path method")
|
||||
// },
|
||||
// PinFunc: func() string {
|
||||
// panic("mock out the Pin method")
|
||||
// },
|
||||
// URLFunc: func() string {
|
||||
// panic("mock out the URL method")
|
||||
// },
|
||||
|
|
@ -54,12 +60,18 @@ type ExtensionMock struct {
|
|||
// IsLocalFunc mocks the IsLocal method.
|
||||
IsLocalFunc func() bool
|
||||
|
||||
// IsPinnedFunc mocks the IsPinned method.
|
||||
IsPinnedFunc func() bool
|
||||
|
||||
// NameFunc mocks the Name method.
|
||||
NameFunc func() string
|
||||
|
||||
// PathFunc mocks the Path method.
|
||||
PathFunc func() string
|
||||
|
||||
// PinFunc mocks the Pin method.
|
||||
PinFunc func() string
|
||||
|
||||
// URLFunc mocks the URL method.
|
||||
URLFunc func() string
|
||||
|
||||
|
|
@ -77,12 +89,18 @@ type ExtensionMock struct {
|
|||
// IsLocal holds details about calls to the IsLocal method.
|
||||
IsLocal []struct {
|
||||
}
|
||||
// IsPinned holds details about calls to the IsPinned method.
|
||||
IsPinned []struct {
|
||||
}
|
||||
// Name holds details about calls to the Name method.
|
||||
Name []struct {
|
||||
}
|
||||
// Path holds details about calls to the Path method.
|
||||
Path []struct {
|
||||
}
|
||||
// Pin holds details about calls to the Pin method.
|
||||
Pin []struct {
|
||||
}
|
||||
// URL holds details about calls to the URL method.
|
||||
URL []struct {
|
||||
}
|
||||
|
|
@ -93,8 +111,10 @@ type ExtensionMock struct {
|
|||
lockCurrentVersion sync.RWMutex
|
||||
lockIsBinary sync.RWMutex
|
||||
lockIsLocal sync.RWMutex
|
||||
lockIsPinned sync.RWMutex
|
||||
lockName sync.RWMutex
|
||||
lockPath sync.RWMutex
|
||||
lockPin sync.RWMutex
|
||||
lockURL sync.RWMutex
|
||||
lockUpdateAvailable sync.RWMutex
|
||||
}
|
||||
|
|
@ -177,6 +197,32 @@ func (mock *ExtensionMock) IsLocalCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// IsPinned calls IsPinnedFunc.
|
||||
func (mock *ExtensionMock) IsPinned() bool {
|
||||
if mock.IsPinnedFunc == nil {
|
||||
panic("ExtensionMock.IsPinnedFunc: method is nil but Extension.IsPinned was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockIsPinned.Lock()
|
||||
mock.calls.IsPinned = append(mock.calls.IsPinned, callInfo)
|
||||
mock.lockIsPinned.Unlock()
|
||||
return mock.IsPinnedFunc()
|
||||
}
|
||||
|
||||
// IsPinnedCalls gets all the calls that were made to IsPinned.
|
||||
// Check the length with:
|
||||
// len(mockedExtension.IsPinnedCalls())
|
||||
func (mock *ExtensionMock) IsPinnedCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
mock.lockIsPinned.RLock()
|
||||
calls = mock.calls.IsPinned
|
||||
mock.lockIsPinned.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Name calls NameFunc.
|
||||
func (mock *ExtensionMock) Name() string {
|
||||
if mock.NameFunc == nil {
|
||||
|
|
@ -229,6 +275,32 @@ func (mock *ExtensionMock) PathCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// Pin calls PinFunc.
|
||||
func (mock *ExtensionMock) Pin() string {
|
||||
if mock.PinFunc == nil {
|
||||
panic("ExtensionMock.PinFunc: method is nil but Extension.Pin was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockPin.Lock()
|
||||
mock.calls.Pin = append(mock.calls.Pin, callInfo)
|
||||
mock.lockPin.Unlock()
|
||||
return mock.PinFunc()
|
||||
}
|
||||
|
||||
// PinCalls gets all the calls that were made to Pin.
|
||||
// Check the length with:
|
||||
// len(mockedExtension.PinCalls())
|
||||
func (mock *ExtensionMock) PinCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
mock.lockPin.RLock()
|
||||
calls = mock.calls.Pin
|
||||
mock.lockPin.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// URL calls URLFunc.
|
||||
func (mock *ExtensionMock) URL() string {
|
||||
if mock.URLFunc == nil {
|
||||
|
|
|
|||
|
|
@ -25,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(interfaceMoqParam ghrepo.Interface) error {
|
||||
// InstallFunc: func(interfaceMoqParam ghrepo.Interface, s string) error {
|
||||
// panic("mock out the Install method")
|
||||
// },
|
||||
// InstallLocalFunc: func(dir string) error {
|
||||
|
|
@ -54,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(interfaceMoqParam ghrepo.Interface) error
|
||||
InstallFunc func(interfaceMoqParam ghrepo.Interface, s string) error
|
||||
|
||||
// InstallLocalFunc mocks the InstallLocal method.
|
||||
InstallLocalFunc func(dir string) error
|
||||
|
|
@ -92,6 +92,8 @@ type ExtensionManagerMock struct {
|
|||
Install []struct {
|
||||
// InterfaceMoqParam is the interfaceMoqParam argument value.
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
// S is the s argument value.
|
||||
S string
|
||||
}
|
||||
// InstallLocal holds details about calls to the InstallLocal method.
|
||||
InstallLocal []struct {
|
||||
|
|
@ -204,19 +206,21 @@ func (mock *ExtensionManagerMock) DispatchCalls() []struct {
|
|||
}
|
||||
|
||||
// Install calls InstallFunc.
|
||||
func (mock *ExtensionManagerMock) Install(interfaceMoqParam ghrepo.Interface) error {
|
||||
func (mock *ExtensionManagerMock) Install(interfaceMoqParam ghrepo.Interface, s string) error {
|
||||
if mock.InstallFunc == nil {
|
||||
panic("ExtensionManagerMock.InstallFunc: method is nil but ExtensionManager.Install was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
S string
|
||||
}{
|
||||
InterfaceMoqParam: interfaceMoqParam,
|
||||
S: s,
|
||||
}
|
||||
mock.lockInstall.Lock()
|
||||
mock.calls.Install = append(mock.calls.Install, callInfo)
|
||||
mock.lockInstall.Unlock()
|
||||
return mock.InstallFunc(interfaceMoqParam)
|
||||
return mock.InstallFunc(interfaceMoqParam, s)
|
||||
}
|
||||
|
||||
// InstallCalls gets all the calls that were made to Install.
|
||||
|
|
@ -224,9 +228,11 @@ func (mock *ExtensionManagerMock) Install(interfaceMoqParam ghrepo.Interface) er
|
|||
// len(mockedExtensionManager.InstallCalls())
|
||||
func (mock *ExtensionManagerMock) InstallCalls() []struct {
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
S string
|
||||
} {
|
||||
var calls []struct {
|
||||
InterfaceMoqParam ghrepo.Interface
|
||||
S string
|
||||
}
|
||||
mock.lockInstall.RLock()
|
||||
calls = mock.calls.Install
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue