pinning binary exts

This commit is contained in:
meiji163 2022-03-01 15:01:04 -08:00
parent e361fd47a3
commit d7277e396c
5 changed files with 90 additions and 40 deletions

View file

@ -76,10 +76,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 +92,44 @@ 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] == "." {
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, pinFlag); 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 io.IsStdoutTTY() {
cs := io.ColorScheme()
fmt.Fprintf(io.Out, "%s Installed extension %s\n", cs.SuccessIcon(), args[0])
}
return nil
},
}
cmd.Flags().StringVar(&pinFlag, "pin", "", "pin extension to a release tag or commit sha")
return cmd
}(),
func() *cobra.Command {
var flagAll bool
var flagForce bool

View file

@ -112,3 +112,37 @@ func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*re
return &r, nil
}
// fetchRelease 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)
defer resp.Body.Close()
if err != nil {
return nil, err
}
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
}

View file

@ -320,13 +320,13 @@ type binManifest struct {
Path string
}
func (m *Manager) Install(repo ghrepo.Interface) error {
func (m *Manager) Install(repo ghrepo.Interface, targetCommitish 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, targetCommitish)
}
hs, err := hasScript(m.client, repo)
@ -341,9 +341,14 @@ func (m *Manager) Install(repo ghrepo.Interface) error {
return m.installGit(ghrepo.FormatRemoteURL(repo, protocol), m.io.Out, m.io.ErrOut)
}
func (m *Manager) installBin(repo ghrepo.Interface) error {
func (m *Manager) installBin(repo ghrepo.Interface, targetCommitish string) error {
var r *release
r, err := fetchLatestRelease(m.client, repo)
var err error
if targetCommitish == "" {
r, err = fetchLatestRelease(m.client, repo)
} else {
r, err = fetchReleaseFromTag(m.client, repo, targetCommitish)
}
if err != nil {
return err
}
@ -498,7 +503,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 +530,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 {

View file

@ -28,7 +28,7 @@ type Extension interface {
//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

View file

@ -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