diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index b19b80b55..50112b717 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -124,6 +124,9 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { } else if errors.Is(err, commitNotFoundErr) { return fmt.Errorf("%s %s does not exist in %s", cs.FailureIcon(), cs.Cyan(pinFlag), args[0]) + } else if errors.Is(err, repositoryNotFoundErr) { + return fmt.Errorf("%s Could not find extension '%s' on host %s", + cs.FailureIcon(), args[0], repo.RepoHost()) } return err } diff --git a/pkg/cmd/extension/command_test.go b/pkg/cmd/extension/command_test.go index fb4707320..46bc109af 100644 --- a/pkg/cmd/extension/command_test.go +++ b/pkg/cmd/extension/command_test.go @@ -87,6 +87,25 @@ func TestNewCmdExtension(t *testing.T) { } }, }, + { + name: "error extension not found", + args: []string{"install", "owner/gh-some-ext"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func() []extensions.Extension { + return []extensions.Extension{} + } + em.InstallFunc = func(_ ghrepo.Interface, _ string) error { + return repositoryNotFoundErr + } + return func(t *testing.T) { + installCalls := em.InstallCalls() + assert.Equal(t, 1, len(installCalls)) + assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName()) + } + }, + wantErr: true, + errMsg: "X Could not find extension 'owner/gh-some-ext' on host github.com", + }, { name: "install local extension with pin", args: []string{"install", ".", "--pin", "v1.0.0"}, diff --git a/pkg/cmd/extension/http.go b/pkg/cmd/extension/http.go index 5e3c23d69..2fae2f023 100644 --- a/pkg/cmd/extension/http.go +++ b/pkg/cmd/extension/http.go @@ -13,32 +13,54 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" ) -func hasScript(httpClient *http.Client, repo ghrepo.Interface) (hs bool, err error) { +func repoExists(httpClient *http.Client, repo ghrepo.Interface) (bool, error) { + url := fmt.Sprintf("%srepos/%s/%s", ghinstance.RESTPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case 200: + return true, nil + case 404: + return false, nil + default: + return false, api.HandleHTTPError(resp) + } +} + +func hasScript(httpClient *http.Client, repo ghrepo.Interface) (bool, 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 + return false, err } resp, err := httpClient.Do(req) if err != nil { - return + return false, err } defer resp.Body.Close() if resp.StatusCode == 404 { - return + return false, nil } if resp.StatusCode > 299 { err = api.HandleHTTPError(resp) - return + return false, err } - hs = true - return + return true, nil } type releaseAsset struct { @@ -80,8 +102,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") +var releaseNotFoundErr = errors.New("release not found") +var repositoryNotFoundErr = errors.New("repository not found") // fetchLatestRelease finds the latest published release for a repository. func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*release, error) { @@ -98,6 +121,9 @@ func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*re } defer resp.Body.Close() + if resp.StatusCode == 404 { + return nil, releaseNotFoundErr + } if resp.StatusCode > 299 { return nil, api.HandleHTTPError(resp) } @@ -162,7 +188,7 @@ func fetchCommitSHA(httpClient *http.Client, baseRepo ghrepo.Interface, targetRe return "", err } - req.Header.Set("Accept", "application/vnd.github.VERSION.sha") + req.Header.Set("Accept", "application/vnd.github.v3.sha") resp, err := httpClient.Do(req) if err != nil { return "", err diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 8ca79537d..289a9aefa 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -339,7 +339,15 @@ type binManifest struct { 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 errors.Is(err, releaseNotFoundErr) { + if ok, err := repoExists(m.client, repo); err != nil { + return err + } else if !ok { + return repositoryNotFoundErr + } + } else { + return fmt.Errorf("could not check for binary extension: %w", err) + } } if isBin { return m.installBin(repo, target) @@ -760,11 +768,6 @@ func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err 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 } diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index e29dc0510..2dbd81870 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -844,6 +844,32 @@ func TestManager_Install_binary(t *testing.T) { assert.Equal(t, "", stderr.String()) } +func TestManager_repo_not_found(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.StatusStringResponse(404, `{}`)) + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext"), + httpmock.StatusStringResponse(404, `{}`)) + + ios, _, stdout, stderr := iostreams.Test() + tempDir := t.TempDir() + + m := newTestManager(tempDir, &http.Client{Transport: ®}, ios) + + if err := m.Install(repo, ""); err != repositoryNotFoundErr { + t.Errorf("expected repositoryNotFoundErr, got: %v", err) + } + + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + func TestManager_Create(t *testing.T) { chdirTemp(t) ios, _, stdout, stderr := iostreams.Test()