X Tutup
Skip to content

Commit 1ec47d8

Browse files
committed
Improvements to gh extensions
- Extensions on Windows now enabled through the `sh.exe` interpreter - `sh.exe` now found on Windows when git was installed via scoop - `gh extensions list` command shows origin repo for the extension - `gh extensions upgrade --all` is required to upgrade all extensions - Added `gh extensions remove` - Shell completions now include aliases and extension names - `gh` help output now lists available extension names - Extensions are stored to XDG_DATA_HOME
1 parent 936d6e1 commit 1ec47d8

File tree

18 files changed

+1064
-67
lines changed

18 files changed

+1064
-67
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
if cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) {

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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
9292

9393
cmd.Flags().BoolVarP(&opts.IsShell, "shell", "s", false, "Declare an alias to be passed through a shell interpreter")
9494

95+
opts.validCommand = func(args string) bool {
96+
split, err := shlex.Split(args)
97+
if err != nil {
98+
return false
99+
}
100+
101+
rootCmd := cmd.Root()
102+
cmd, _, err := rootCmd.Traverse(split)
103+
if err == nil && cmd != rootCmd {
104+
return true
105+
}
106+
107+
for _, ext := range f.ExtensionManager.List() {
108+
if ext.Name() == split[0] {
109+
return true
110+
}
111+
}
112+
return false
113+
}
114+
95115
return cmd
96116
}
97117

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)
X Tutup