Skip to content

Commit 33c3fb5

Browse files
authored
Merge pull request cli#3870 from cli/extensions-revisited
Improvements to gh extensions
2 parents 7fc0acd + 6c984f4 commit 33c3fb5

File tree

18 files changed

+1070
-82
lines changed

18 files changed

+1070
-82
lines changed

cmd/gh/main.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"github.com/cli/cli/internal/run"
2222
"github.com/cli/cli/internal/update"
2323
"github.com/cli/cli/pkg/cmd/alias/expand"
24-
"github.com/cli/cli/pkg/cmd/extensions"
2524
"github.com/cli/cli/pkg/cmd/factory"
2625
"github.com/cli/cli/pkg/cmd/root"
2726
"github.com/cli/cli/pkg/cmdutil"
@@ -143,7 +142,7 @@ func mainRun() exitCode {
143142

144143
return exitOK
145144
} else if c, _, err := rootCmd.Traverse(expandedArgs); err == nil && c == rootCmd && len(expandedArgs) > 0 {
146-
extensionManager := extensions.NewManager()
145+
extensionManager := cmdFactory.ExtensionManager
147146
if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil {
148147
var execError *exec.ExitError
149148
if errors.As(err, &execError) {
@@ -157,6 +156,24 @@ func mainRun() exitCode {
157156
}
158157
}
159158

159+
// provide completions for aliases and extensions
160+
rootCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
161+
var results []string
162+
if aliases, err := cfg.Aliases(); err == nil {
163+
for aliasName := range aliases.All() {
164+
if strings.HasPrefix(aliasName, toComplete) {
165+
results = append(results, aliasName)
166+
}
167+
}
168+
}
169+
for _, ext := range cmdFactory.ExtensionManager.List() {
170+
if strings.HasPrefix(ext.Name(), toComplete) {
171+
results = append(results, ext.Name())
172+
}
173+
}
174+
return results, cobra.ShellCompDirectiveNoFileComp
175+
}
176+
160177
cs := cmdFactory.IOStreams.ColorScheme()
161178

162179
authError := errors.New("authError")

pkg/cmd/alias/expand/expand.go

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ package expand
33
import (
44
"errors"
55
"fmt"
6-
"os"
7-
"path/filepath"
6+
"os/exec"
87
"regexp"
98
"runtime"
109
"strings"
1110

1211
"github.com/cli/cli/internal/config"
13-
"github.com/cli/safeexec"
12+
"github.com/cli/cli/pkg/findsh"
1413
"github.com/google/shlex"
1514
)
1615

@@ -80,27 +79,15 @@ func ExpandAlias(cfg config.Config, args []string, findShFunc func() (string, er
8079
}
8180

8281
func findSh() (string, error) {
83-
shPath, err := safeexec.LookPath("sh")
84-
if err == nil {
85-
return shPath, nil
86-
}
87-
88-
if runtime.GOOS == "windows" {
89-
winNotFoundErr := errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.")
90-
// We can try and find a sh executable in a Git for Windows install
91-
gitPath, err := safeexec.LookPath("git")
92-
if err != nil {
93-
return "", winNotFoundErr
94-
}
95-
96-
shPath = filepath.Join(filepath.Dir(gitPath), "..", "bin", "sh.exe")
97-
_, err = os.Stat(shPath)
98-
if err != nil {
99-
return "", winNotFoundErr
82+
shPath, err := findsh.Find()
83+
if err != nil {
84+
if errors.Is(err, exec.ErrNotFound) {
85+
if runtime.GOOS == "windows" {
86+
return "", errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.")
87+
}
88+
return "", errors.New("unable to locate sh to execute shell alias with")
10089
}
101-
102-
return shPath, nil
90+
return "", err
10391
}
104-
105-
return "", errors.New("unable to locate sh to execute shell alias with")
92+
return shPath, nil
10693
}

pkg/cmd/alias/set/set.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ type SetOptions struct {
2020
Name string
2121
Expansion string
2222
IsShell bool
23-
RootCmd *cobra.Command
23+
24+
validCommand func(string) bool
2425
}
2526

2627
func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
@@ -78,11 +79,29 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
7879
`),
7980
Args: cobra.ExactArgs(2),
8081
RunE: func(cmd *cobra.Command, args []string) error {
81-
opts.RootCmd = cmd.Root()
82-
8382
opts.Name = args[0]
8483
opts.Expansion = args[1]
8584

85+
opts.validCommand = func(args string) bool {
86+
split, err := shlex.Split(args)
87+
if err != nil {
88+
return false
89+
}
90+
91+
rootCmd := cmd.Root()
92+
cmd, _, err := rootCmd.Traverse(split)
93+
if err == nil && cmd != rootCmd {
94+
return true
95+
}
96+
97+
for _, ext := range f.ExtensionManager.List() {
98+
if ext.Name() == split[0] {
99+
return true
100+
}
101+
}
102+
return false
103+
}
104+
86105
if runF != nil {
87106
return runF(opts)
88107
}
@@ -123,11 +142,11 @@ func setRun(opts *SetOptions) error {
123142
}
124143
isShell = strings.HasPrefix(expansion, "!")
125144

126-
if validCommand(opts.RootCmd, opts.Name) {
145+
if opts.validCommand(opts.Name) {
127146
return fmt.Errorf("could not create alias: %q is already a gh command", opts.Name)
128147
}
129148

130-
if !isShell && !validCommand(opts.RootCmd, expansion) {
149+
if !isShell && !opts.validCommand(expansion) {
131150
return fmt.Errorf("could not create alias: %s does not correspond to a gh command", expansion)
132151
}
133152

@@ -153,16 +172,6 @@ func setRun(opts *SetOptions) error {
153172
return nil
154173
}
155174

156-
func validCommand(rootCmd *cobra.Command, expansion string) bool {
157-
split, err := shlex.Split(expansion)
158-
if err != nil {
159-
return false
160-
}
161-
162-
cmd, _, err := rootCmd.Traverse(split)
163-
return err == nil && cmd != rootCmd
164-
}
165-
166175
func getExpansion(opts *SetOptions) (string, error) {
167176
if opts.Expansion == "-" {
168177
stdin, err := ioutil.ReadAll(opts.IO.In)

pkg/cmd/alias/set/set_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/MakeNowJust/heredoc"
99
"github.com/cli/cli/internal/config"
1010
"github.com/cli/cli/pkg/cmdutil"
11+
"github.com/cli/cli/pkg/extensions"
1112
"github.com/cli/cli/pkg/iostreams"
1213
"github.com/cli/cli/test"
1314
"github.com/google/shlex"
@@ -28,6 +29,11 @@ func runCommand(cfg config.Config, isTTY bool, cli string, in string) (*test.Cmd
2829
Config: func() (config.Config, error) {
2930
return cfg, nil
3031
},
32+
ExtensionManager: &extensions.ExtensionManagerMock{
33+
ListFunc: func() []extensions.Extension {
34+
return []extensions.Extension{}
35+
},
36+
},
3137
}
3238

3339
cmd := NewCmdSet(factory, nil)

pkg/cmd/extensions/command.go

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,32 @@ import (
44
"errors"
55
"fmt"
66
"os"
7-
"path/filepath"
87
"strings"
98

9+
"github.com/MakeNowJust/heredoc"
10+
"github.com/cli/cli/git"
1011
"github.com/cli/cli/internal/ghrepo"
1112
"github.com/cli/cli/pkg/cmdutil"
12-
"github.com/cli/cli/pkg/iostreams"
13+
"github.com/cli/cli/utils"
1314
"github.com/spf13/cobra"
1415
)
1516

16-
func NewCmdExtensions(io *iostreams.IOStreams) *cobra.Command {
17-
m := NewManager()
17+
func NewCmdExtensions(f *cmdutil.Factory) *cobra.Command {
18+
m := f.ExtensionManager
19+
io := f.IOStreams
1820

1921
extCmd := cobra.Command{
2022
Use: "extensions",
2123
Short: "Manage gh extensions",
24+
Long: heredoc.Docf(`
25+
GitHub CLI extensions are repositories that provide additional gh commands.
26+
27+
The name of the extension repository must start with "gh-" and it must contain an
28+
executable of the same name. All arguments passed to the %[1]sgh <extname>%[1]s invocation
29+
will be forwarded to the %[1]sgh-<extname>%[1]s executable of the extension.
30+
31+
An extension cannot override any of the core gh commands.
32+
`, "`"),
2233
}
2334

2435
extCmd.AddCommand(
@@ -31,12 +42,23 @@ func NewCmdExtensions(io *iostreams.IOStreams) *cobra.Command {
3142
if len(cmds) == 0 {
3243
return errors.New("no extensions installed")
3344
}
45+
// cs := io.ColorScheme()
46+
t := utils.NewTablePrinter(io)
3447
for _, c := range cmds {
35-
name := filepath.Base(c)
36-
parts := strings.SplitN(name, "-", 2)
37-
fmt.Fprintf(io.Out, "%s %s\n", parts[0], parts[1])
48+
var repo string
49+
if u, err := git.ParseURL(c.URL()); err == nil {
50+
if r, err := ghrepo.FromURL(u); err == nil {
51+
repo = ghrepo.FullName(r)
52+
}
53+
}
54+
55+
t.AddField(fmt.Sprintf("gh %s", c.Name()), nil, nil)
56+
t.AddField(repo, nil, nil)
57+
// TODO: add notice about available update
58+
//t.AddField("Update available", nil, cs.Green)
59+
t.EndRow()
3860
}
39-
return nil
61+
return t.Render()
4062
},
4163
},
4264
&cobra.Command{
@@ -58,16 +80,48 @@ func NewCmdExtensions(io *iostreams.IOStreams) *cobra.Command {
5880
if !strings.HasPrefix(repo.RepoName(), "gh-") {
5981
return errors.New("the repository name must start with `gh-`")
6082
}
61-
protocol := "https" // TODO: respect user's preferred protocol
83+
cfg, err := f.Config()
84+
if err != nil {
85+
return err
86+
}
87+
protocol, _ := cfg.Get(repo.RepoHost(), "git_protocol")
6288
return m.Install(ghrepo.FormatRemoteURL(repo, protocol), io.Out, io.ErrOut)
6389
},
6490
},
91+
func() *cobra.Command {
92+
var flagAll bool
93+
cmd := &cobra.Command{
94+
Use: "upgrade {<name> | --all}",
95+
Short: "Upgrade installed extensions",
96+
Args: func(cmd *cobra.Command, args []string) error {
97+
if len(args) == 0 && !flagAll {
98+
return &cmdutil.FlagError{Err: errors.New("must specify an extension to upgrade")}
99+
}
100+
if len(args) > 0 && flagAll {
101+
return &cmdutil.FlagError{Err: errors.New("cannot use `--all` with extension name")}
102+
}
103+
if len(args) > 1 {
104+
return &cmdutil.FlagError{Err: errors.New("too many arguments")}
105+
}
106+
return nil
107+
},
108+
RunE: func(cmd *cobra.Command, args []string) error {
109+
var name string
110+
if len(args) > 0 {
111+
name = args[0]
112+
}
113+
return m.Upgrade(name, io.Out, io.ErrOut)
114+
},
115+
}
116+
cmd.Flags().BoolVar(&flagAll, "all", false, "Upgrade all extensions")
117+
return cmd
118+
}(),
65119
&cobra.Command{
66-
Use: "upgrade",
67-
Short: "Upgrade installed extensions",
68-
Args: cobra.NoArgs,
120+
Use: "remove",
121+
Short: "Remove an installed extension",
122+
Args: cobra.ExactArgs(1),
69123
RunE: func(cmd *cobra.Command, args []string) error {
70-
return m.Upgrade(io.Out, io.ErrOut)
124+
return m.Remove(args[0])
71125
},
72126
},
73127
)

0 commit comments

Comments
 (0)