Merge pull request #3761 from cli/command-extensions
Experimental command extensions support
This commit is contained in:
commit
ffebd23ba7
4 changed files with 216 additions and 3 deletions
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/internal/update"
|
||||
"github.com/cli/cli/pkg/cmd/alias/expand"
|
||||
"github.com/cli/cli/pkg/cmd/extensions"
|
||||
"github.com/cli/cli/pkg/cmd/factory"
|
||||
"github.com/cli/cli/pkg/cmd/root"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
|
|
@ -140,15 +141,27 @@ func mainRun() exitCode {
|
|||
|
||||
err = preparedCmd.Run()
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return exitCode(ee.ExitCode())
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
|
||||
fmt.Fprintf(stderr, "failed to run external command: %s", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
return exitOK
|
||||
} else if c, _, err := rootCmd.Traverse(expandedArgs); err == nil && c == rootCmd && len(expandedArgs) > 0 {
|
||||
extensionManager := extensions.NewManager()
|
||||
if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(stderr, "failed to run extension: %s", err)
|
||||
return exitError
|
||||
} else if found {
|
||||
return exitOK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
77
pkg/cmd/extensions/command.go
Normal file
77
pkg/cmd/extensions/command.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdExtensions(io *iostreams.IOStreams) *cobra.Command {
|
||||
m := NewManager()
|
||||
|
||||
extCmd := cobra.Command{
|
||||
Use: "extensions",
|
||||
Short: "Manage gh extensions",
|
||||
}
|
||||
|
||||
extCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List installed extension commands",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cmds := m.List()
|
||||
if len(cmds) == 0 {
|
||||
return errors.New("no extensions installed")
|
||||
}
|
||||
for _, c := range cmds {
|
||||
name := filepath.Base(c)
|
||||
parts := strings.SplitN(name, "-", 2)
|
||||
fmt.Fprintf(io.Out, "%s %s\n", parts[0], parts[1])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "install <repo>",
|
||||
Short: "Install a gh extension from a repository",
|
||||
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
|
||||
}
|
||||
if !strings.HasPrefix(repo.RepoName(), "gh-") {
|
||||
return errors.New("the repository name must start with `gh-`")
|
||||
}
|
||||
protocol := "https" // TODO: respect user's preferred protocol
|
||||
return m.Install(ghrepo.FormatRemoteURL(repo, protocol), io.Out, io.ErrOut)
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "upgrade",
|
||||
Short: "Upgrade installed extensions",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return m.Upgrade(io.Out, io.ErrOut)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
extCmd.Hidden = true
|
||||
return &extCmd
|
||||
}
|
||||
121
pkg/cmd/extensions/manager.go
Normal file
121
pkg/cmd/extensions/manager.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
dataDir func() string
|
||||
lookPath func(string) (string, error)
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
dataDir: config.ConfigDir,
|
||||
lookPath: safeexec.LookPath,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
var exe string
|
||||
extName := "gh-" + args[0]
|
||||
forwardArgs := args[1:]
|
||||
|
||||
for _, e := range m.List() {
|
||||
if filepath.Base(e) == extName {
|
||||
exe = e
|
||||
break
|
||||
}
|
||||
}
|
||||
if exe == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// TODO: parse the shebang on Windows and invoke the correct interpreter instead of invoking directly
|
||||
externalCmd := exec.Command(exe, forwardArgs...)
|
||||
externalCmd.Stdin = stdin
|
||||
externalCmd.Stdout = stdout
|
||||
externalCmd.Stderr = stderr
|
||||
return true, externalCmd.Run()
|
||||
}
|
||||
|
||||
func (m *Manager) List() []string {
|
||||
dir := m.installDir()
|
||||
entries, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var results []string
|
||||
for _, f := range entries {
|
||||
if !strings.HasPrefix(f.Name(), "gh-") || !(f.IsDir() || f.Mode()&os.ModeSymlink != 0) {
|
||||
continue
|
||||
}
|
||||
results = append(results, filepath.Join(dir, f.Name(), f.Name()))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (m *Manager) InstallLocal(dir string) error {
|
||||
name := filepath.Base(dir)
|
||||
targetDir := filepath.Join(m.installDir(), name)
|
||||
return os.Symlink(dir, targetDir)
|
||||
}
|
||||
|
||||
func (m *Manager) Install(cloneURL string, stdout, stderr io.Writer) error {
|
||||
exe, err := m.lookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(path.Base(cloneURL), ".git")
|
||||
targetDir := filepath.Join(m.installDir(), name)
|
||||
|
||||
externalCmd := exec.Command(exe, "clone", cloneURL, targetDir)
|
||||
externalCmd.Stdout = stdout
|
||||
externalCmd.Stderr = stderr
|
||||
return externalCmd.Run()
|
||||
}
|
||||
|
||||
func (m *Manager) Upgrade(stdout, stderr io.Writer) error {
|
||||
exe, err := m.lookPath("git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exts := m.List()
|
||||
if len(exts) == 0 {
|
||||
return errors.New("no extensions installed")
|
||||
}
|
||||
|
||||
for _, f := range exts {
|
||||
fmt.Fprintf(stdout, "[%s]: ", filepath.Base(f))
|
||||
dir := filepath.Dir(f)
|
||||
externalCmd := exec.Command(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only")
|
||||
externalCmd.Stdout = stdout
|
||||
externalCmd.Stderr = stderr
|
||||
if e := externalCmd.Run(); e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) installDir() string {
|
||||
return filepath.Join(m.dataDir(), "extensions")
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import (
|
|||
authCmd "github.com/cli/cli/pkg/cmd/auth"
|
||||
completionCmd "github.com/cli/cli/pkg/cmd/completion"
|
||||
configCmd "github.com/cli/cli/pkg/cmd/config"
|
||||
extensionsCmd "github.com/cli/cli/pkg/cmd/extensions"
|
||||
"github.com/cli/cli/pkg/cmd/factory"
|
||||
gistCmd "github.com/cli/cli/pkg/cmd/gist"
|
||||
issueCmd "github.com/cli/cli/pkg/cmd/issue"
|
||||
|
|
@ -80,6 +81,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil))
|
||||
cmd.AddCommand(gistCmd.NewCmdGist(f))
|
||||
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
|
||||
cmd.AddCommand(extensionsCmd.NewCmdExtensions(f.IOStreams))
|
||||
cmd.AddCommand(secretCmd.NewCmdSecret(f))
|
||||
cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue