From 4a2c5f222a1041f377d60764413cc4fb0ec20a23 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 4 Nov 2022 11:20:54 -0700 Subject: [PATCH 1/5] add gh ext search --- pkg/cmd/extension/command.go | 148 ++++++++++++++++++ pkg/cmd/extension/command_test.go | 240 ++++++++++++++++++++++++++++++ 2 files changed, 388 insertions(+) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 995e4bfd4..7625d7254 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -9,8 +9,11 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/tableprinter" + "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/search" "github.com/cli/cli/v2/utils" "github.com/spf13/cobra" ) @@ -19,6 +22,9 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { m := f.ExtensionManager io := f.IOStreams prompter := f.Prompter + config := f.Config + browser := f.Browser + httpClient := f.HttpClient extCmd := cobra.Command{ Use: "extension", @@ -39,6 +45,148 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { } extCmd.AddCommand( + func() *cobra.Command { + query := search.Query{ + Kind: search.KindRepositories, + } + qualifiers := search.Qualifiers{ + Topic: []string{"gh-extension"}, + } + var order string + var sort string + var webMode bool + var exporter cmdutil.Exporter + + cmd := &cobra.Command{ + Use: "search []", + Short: "Search available gh extensions", + Long: heredoc.Doc(` + Search for gh extensions. + + This command behaves similarly to 'gh search repos' but does not + support as many search qualifiers. For a finer grained search of + extensions, try using: + + gh search repos --topic "gh-extension" + + and adding qualifiers as needed. See 'gh help search repos' to learn + more about repository search. +`), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config() + if err != nil { + return err + } + client, err := httpClient() + if err != nil { + return err + } + + if cmd.Flags().Changed("order") { + query.Order = order + } + if cmd.Flags().Changed("sort") { + query.Sort = sort + } + + query.Keywords = args + query.Qualifiers = qualifiers + + host, _ := cfg.DefaultHost() + searcher := search.NewSearcher(client, host) + tp := tableprinter.New(io) + + if webMode { + url := searcher.URL(query) + if io.IsStdoutTTY() { + fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url)) + } + return browser.Browse(url) + } + + result, err := searcher.Repositories(query) + if err != nil { + return err + } + + if io.CanPrompt() { + fmt.Fprintf(io.Out, "Showing %d of %d gh extensions\n", query.Limit, result.Total) + fmt.Fprintln(io.Out) + tp.HeaderRow("NAME", "REPO", "INSTALLED", "OFFICIAL", "DESCRIPTION") + } + + cs := io.ColorScheme() + installedExts := m.List() + + isInstalled := func(repoFullName string) bool { + for _, e := range installedExts { + // TODO consider a Repo() on Extension interface + var installedRepo string + if u, err := git.ParseURL(e.URL()); err == nil { + if r, err := ghrepo.FromURL(u); err == nil { + installedRepo = ghrepo.FullName(r) + } + } + if repoFullName == installedRepo { + return true + } + } + return false + } + + for _, repo := range result.Items { + if !strings.HasPrefix(repo.Name, "gh-") { + continue + } + + official := "" + if repo.Owner.Login == "cli" || repo.Owner.Login == "github" { + if io.IsStdoutTTY() { + official = "✓" + } else { + official = "official" + } + } + + installed := "" + if isInstalled(repo.FullName) { + if io.IsStdoutTTY() { + installed = "✓" + } else { + installed = "installed" + } + } + + cmdName := strings.TrimPrefix(repo.Name, "gh-") + + tp.AddField(cmdName, tableprinter.WithColor(cs.Bold)) + tp.AddField(repo.FullName) + tp.AddField(installed, tableprinter.WithColor(cs.Green)) + tp.AddField(official, tableprinter.WithColor(cs.Yellow)) + tp.AddField(repo.Description) + tp.EndRow() + } + + return tp.Render() + }, + } + + // Output flags + cmd.Flags().BoolVarP(&webMode, "web", "w", false, "Open the search query in the web browser") + cmdutil.AddJSONFlags(cmd, &exporter, search.RepositoryFields) + + // Query parameter flags + cmd.Flags().IntVarP(&query.Limit, "limit", "L", 30, "Maximum number of extensions to fetch") + cmdutil.StringEnumFlag(cmd, &order, "order", "", "desc", []string{"asc", "desc"}, "Order of repositories returned, ignored unless '--sort' flag is specified") + cmdutil.StringEnumFlag(cmd, &sort, "sort", "", "best-match", []string{"forks", "help-wanted-issues", "stars", "updated"}, "Sort fetched repositories") + + // Qualifier flags + cmd.Flags().StringVar(&qualifiers.Language, "language", "", "Filter based on the coding language") + cmd.Flags().StringSliceVar(&qualifiers.License, "license", nil, "Filter based on license type") + cmd.Flags().StringVar(&qualifiers.User, "owner", "", "Filter on owner") + + return cmd + }(), &cobra.Command{ Use: "list", Short: "List installed extension commands", diff --git a/pkg/cmd/extension/command_test.go b/pkg/cmd/extension/command_test.go index 46bc109af..969ccd2b8 100644 --- a/pkg/cmd/extension/command_test.go +++ b/pkg/cmd/extension/command_test.go @@ -5,11 +5,13 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strings" "testing" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" @@ -17,6 +19,7 @@ import ( "github.com/cli/cli/v2/pkg/extensions" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/search" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -32,12 +35,193 @@ func TestNewCmdExtension(t *testing.T) { args []string managerStubs func(em *extensions.ExtensionManagerMock) func(*testing.T) prompterStubs func(pm *prompter.PrompterMock) + httpStubs func(reg *httpmock.Registry) + browseStubs func(*browser.Stub) func(*testing.T) isTTY bool wantErr bool errMsg string wantStdout string wantStderr string }{ + { + name: "search for extensions", + args: []string{"search"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func() []extensions.Extension { + return []extensions.Extension{ + &extensions.ExtensionMock{ + URLFunc: func() string { + return "https://github.com/vilmibm/gh-screensaver" + }, + }, + &extensions.ExtensionMock{ + URLFunc: func() string { + return "https://github.com/github/gh-gei" + }, + }, + } + } + return func(t *testing.T) { + listCalls := em.ListCalls() + assert.Equal(t, 1, len(listCalls)) + } + }, + httpStubs: func(reg *httpmock.Registry) { + values := url.Values{ + "page": []string{"1"}, + "per_page": []string{"30"}, + "q": []string{"topic:gh-extension"}, + } + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.JSONResponse(searchResults()), + ) + }, + isTTY: true, + wantStdout: "screensaver vilmibm/gh-screensaver ✓ terminal animations\ncool cli/gh-cool ✓ it's just cool ok\ntriage samcoe/gh-triage helps with triage\ngei github/gh-gei ✓ ✓ something something enterprise\n", + }, + { + name: "search for extensions non-tty", + args: []string{"search"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func() []extensions.Extension { + return []extensions.Extension{ + &extensions.ExtensionMock{ + URLFunc: func() string { + return "https://github.com/vilmibm/gh-screensaver" + }, + }, + &extensions.ExtensionMock{ + URLFunc: func() string { + return "https://github.com/github/gh-gei" + }, + }, + } + } + return func(t *testing.T) { + listCalls := em.ListCalls() + assert.Equal(t, 1, len(listCalls)) + } + }, + httpStubs: func(reg *httpmock.Registry) { + values := url.Values{ + "page": []string{"1"}, + "per_page": []string{"30"}, + "q": []string{"topic:gh-extension"}, + } + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.JSONResponse(searchResults()), + ) + }, + wantStdout: "screensaver\tvilmibm/gh-screensaver\tinstalled\t\tterminal animations\ncool\tcli/gh-cool\t\tofficial\tit's just cool ok\ntriage\tsamcoe/gh-triage\t\t\thelps with triage\ngei\tgithub/gh-gei\tinstalled\tofficial\tsomething something enterprise\n", + }, + { + name: "search for extensions with keywords", + args: []string{"search", "screen"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func() []extensions.Extension { + return []extensions.Extension{ + &extensions.ExtensionMock{ + URLFunc: func() string { + return "https://github.com/vilmibm/gh-screensaver" + }, + }, + &extensions.ExtensionMock{ + URLFunc: func() string { + return "https://github.com/github/gh-gei" + }, + }, + } + } + return func(t *testing.T) { + listCalls := em.ListCalls() + assert.Equal(t, 1, len(listCalls)) + } + }, + httpStubs: func(reg *httpmock.Registry) { + values := url.Values{ + "page": []string{"1"}, + "per_page": []string{"30"}, + "q": []string{"screen topic:gh-extension"}, + } + results := searchResults() + results.Total = 1 + results.Items = []search.Repository{results.Items[0]} + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.JSONResponse(results), + ) + }, + wantStdout: "screensaver\tvilmibm/gh-screensaver\tinstalled\t\tterminal animations\n", + }, + { + name: "search for extensions with parameter flags", + args: []string{"search", "--limit", "1", "--order", "asc", "--sort", "stars"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func() []extensions.Extension { + return []extensions.Extension{} + } + return func(t *testing.T) { + listCalls := em.ListCalls() + assert.Equal(t, 1, len(listCalls)) + } + }, + httpStubs: func(reg *httpmock.Registry) { + values := url.Values{ + "page": []string{"1"}, + "order": []string{"asc"}, + "sort": []string{"stars"}, + "per_page": []string{"1"}, + "q": []string{"topic:gh-extension"}, + } + results := searchResults() + results.Total = 1 + results.Items = []search.Repository{results.Items[0]} + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.JSONResponse(results), + ) + }, + wantStdout: "screensaver\tvilmibm/gh-screensaver\t\t\tterminal animations\n", + }, + { + name: "search for extensions with qualifier flags", + args: []string{"search", "--language", "Go", "--license", "GPLv3", "--owner", "jillvalentine"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.ListFunc = func() []extensions.Extension { + return []extensions.Extension{} + } + return func(t *testing.T) { + listCalls := em.ListCalls() + assert.Equal(t, 1, len(listCalls)) + } + }, + httpStubs: func(reg *httpmock.Registry) { + values := url.Values{ + "page": []string{"1"}, + "per_page": []string{"30"}, + "q": []string{"language:Go license:GPLv3 topic:gh-extension user:jillvalentine"}, + } + results := searchResults() + results.Total = 1 + results.Items = []search.Repository{results.Items[0]} + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.JSONResponse(results), + ) + }, + wantStdout: "screensaver\tvilmibm/gh-screensaver\t\t\tterminal animations\n", + }, + { + name: "search for extensions with web mode", + args: []string{"search", "--web"}, + browseStubs: func(b *browser.Stub) func(*testing.T) { + return func(t *testing.T) { + b.Verify(t, "https://github.com/search?q=topic%3Agh-extension&type=repositories") + } + }, + }, { name: "install an extension", args: []string{"install", "owner/gh-some-ext"}, @@ -595,6 +779,16 @@ func TestNewCmdExtension(t *testing.T) { defer reg.Verify(t) client := http.Client{Transport: ®} + if tt.httpStubs != nil { + tt.httpStubs(®) + } + + var assertBrowserFunc func(*testing.T) + browseStub := &browser.Stub{} + if tt.browseStubs != nil { + assertBrowserFunc = tt.browseStubs(browseStub) + } + f := cmdutil.Factory{ Config: func() (config.Config, error) { return config.NewBlankConfig(), nil @@ -602,6 +796,7 @@ func TestNewCmdExtension(t *testing.T) { IOStreams: ios, ExtensionManager: em, Prompter: pm, + Browser: browseStub, HttpClient: func() (*http.Client, error) { return &client, nil }, @@ -623,6 +818,10 @@ func TestNewCmdExtension(t *testing.T) { assertFunc(t) } + if assertBrowserFunc != nil { + assertBrowserFunc(t) + } + assert.Equal(t, tt.wantStdout, stdout.String()) assert.Equal(t, tt.wantStderr, stderr.String()) }) @@ -708,3 +907,44 @@ func Test_checkValidExtension(t *testing.T) { }) } } + +func searchResults() search.RepositoriesResult { + return search.RepositoriesResult{ + IncompleteResults: false, + Items: []search.Repository{ + { + FullName: "vilmibm/gh-screensaver", + Name: "gh-screensaver", + Description: "terminal animations", + Owner: search.User{ + Login: "vilmibm", + }, + }, + { + FullName: "cli/gh-cool", + Name: "gh-cool", + Description: "it's just cool ok", + Owner: search.User{ + Login: "cli", + }, + }, + { + FullName: "samcoe/gh-triage", + Name: "gh-triage", + Description: "helps with triage", + Owner: search.User{ + Login: "samcoe", + }, + }, + { + FullName: "github/gh-gei", + Name: "gh-gei", + Description: "something something enterprise", + Owner: search.User{ + Login: "github", + }, + }, + }, + Total: 4, + } +} From 869248fc53214b4ca179a1a503997ffcd3f00e06 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 7 Nov 2022 14:59:26 -0800 Subject: [PATCH 2/5] review feedback --- pkg/cmd/extension/command.go | 66 +++++++++++++++---------------- pkg/cmd/extension/command_test.go | 14 +++---- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 7625d7254..552a9d1a5 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -61,17 +61,17 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { Use: "search []", Short: "Search available gh extensions", Long: heredoc.Doc(` - Search for gh extensions. + Search for gh extensions. - This command behaves similarly to 'gh search repos' but does not - support as many search qualifiers. For a finer grained search of - extensions, try using: + This command behaves similarly to 'gh search repos' but does not + support as many search qualifiers. For a finer grained search of + extensions, try using: - gh search repos --topic "gh-extension" + gh search repos --topic "gh-extension" - and adding qualifiers as needed. See 'gh help search repos' to learn - more about repository search. -`), + and adding qualifiers as needed. See 'gh help search repos' to learn + more about repository search. + `), RunE: func(cmd *cobra.Command, args []string) error { cfg, err := config() if err != nil { @@ -94,7 +94,6 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { host, _ := cfg.DefaultHost() searcher := search.NewSearcher(client, host) - tp := tableprinter.New(io) if webMode { url := searcher.URL(query) @@ -104,52 +103,56 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { return browser.Browse(url) } + io.StartProgressIndicator() result, err := searcher.Repositories(query) + io.StopProgressIndicator() if err != nil { return err } - if io.CanPrompt() { - fmt.Fprintf(io.Out, "Showing %d of %d gh extensions\n", query.Limit, result.Total) + if exporter != nil { + return exporter.Write(io, result.Items) + } + + if io.IsStdoutTTY() { + if len(result.Items) == 0 { + return errors.New("no extensions found") + } + fmt.Fprintf(io.Out, "Showing %d of %d extensions\n", len(result.Items), result.Total) fmt.Fprintln(io.Out) - tp.HeaderRow("NAME", "REPO", "INSTALLED", "OFFICIAL", "DESCRIPTION") } cs := io.ColorScheme() installedExts := m.List() - isInstalled := func(repoFullName string) bool { + isInstalled := func(repo search.Repository) bool { + searchRepo, err := ghrepo.FromFullName(repo.FullName) + if err != nil { + return false + } for _, e := range installedExts { // TODO consider a Repo() on Extension interface - var installedRepo string if u, err := git.ParseURL(e.URL()); err == nil { if r, err := ghrepo.FromURL(u); err == nil { - installedRepo = ghrepo.FullName(r) + if ghrepo.IsSame(searchRepo, r) { + return true + } } } - if repoFullName == installedRepo { - return true - } } return false } + tp := tableprinter.New(io) + tp.HeaderRow("", "REPO", "DESCRIPTION") + for _, repo := range result.Items { if !strings.HasPrefix(repo.Name, "gh-") { continue } - official := "" - if repo.Owner.Login == "cli" || repo.Owner.Login == "github" { - if io.IsStdoutTTY() { - official = "✓" - } else { - official = "official" - } - } - installed := "" - if isInstalled(repo.FullName) { + if isInstalled(repo) { if io.IsStdoutTTY() { installed = "✓" } else { @@ -157,12 +160,8 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { } } - cmdName := strings.TrimPrefix(repo.Name, "gh-") - - tp.AddField(cmdName, tableprinter.WithColor(cs.Bold)) - tp.AddField(repo.FullName) tp.AddField(installed, tableprinter.WithColor(cs.Green)) - tp.AddField(official, tableprinter.WithColor(cs.Yellow)) + tp.AddField(repo.FullName, tableprinter.WithColor(cs.Bold)) tp.AddField(repo.Description) tp.EndRow() } @@ -181,7 +180,6 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { cmdutil.StringEnumFlag(cmd, &sort, "sort", "", "best-match", []string{"forks", "help-wanted-issues", "stars", "updated"}, "Sort fetched repositories") // Qualifier flags - cmd.Flags().StringVar(&qualifiers.Language, "language", "", "Filter based on the coding language") cmd.Flags().StringSliceVar(&qualifiers.License, "license", nil, "Filter based on license type") cmd.Flags().StringVar(&qualifiers.User, "owner", "", "Filter on owner") diff --git a/pkg/cmd/extension/command_test.go b/pkg/cmd/extension/command_test.go index 969ccd2b8..7b2533da3 100644 --- a/pkg/cmd/extension/command_test.go +++ b/pkg/cmd/extension/command_test.go @@ -78,7 +78,7 @@ func TestNewCmdExtension(t *testing.T) { ) }, isTTY: true, - wantStdout: "screensaver vilmibm/gh-screensaver ✓ terminal animations\ncool cli/gh-cool ✓ it's just cool ok\ntriage samcoe/gh-triage helps with triage\ngei github/gh-gei ✓ ✓ something something enterprise\n", + wantStdout: "Showing 4 of 4 extensions\n\n REPO DESCRIPTION\n✓ vilmibm/gh-screensaver terminal animations\n cli/gh-cool it's just cool ok\n samcoe/gh-triage helps with triage\n✓ github/gh-gei something something enterprise\n", }, { name: "search for extensions non-tty", @@ -114,7 +114,7 @@ func TestNewCmdExtension(t *testing.T) { httpmock.JSONResponse(searchResults()), ) }, - wantStdout: "screensaver\tvilmibm/gh-screensaver\tinstalled\t\tterminal animations\ncool\tcli/gh-cool\t\tofficial\tit's just cool ok\ntriage\tsamcoe/gh-triage\t\t\thelps with triage\ngei\tgithub/gh-gei\tinstalled\tofficial\tsomething something enterprise\n", + wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n\tcli/gh-cool\tit's just cool ok\n\tsamcoe/gh-triage\thelps with triage\ninstalled\tgithub/gh-gei\tsomething something enterprise\n", }, { name: "search for extensions with keywords", @@ -153,7 +153,7 @@ func TestNewCmdExtension(t *testing.T) { httpmock.JSONResponse(results), ) }, - wantStdout: "screensaver\tvilmibm/gh-screensaver\tinstalled\t\tterminal animations\n", + wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n", }, { name: "search for extensions with parameter flags", @@ -183,11 +183,11 @@ func TestNewCmdExtension(t *testing.T) { httpmock.JSONResponse(results), ) }, - wantStdout: "screensaver\tvilmibm/gh-screensaver\t\t\tterminal animations\n", + wantStdout: "\tvilmibm/gh-screensaver\tterminal animations\n", }, { name: "search for extensions with qualifier flags", - args: []string{"search", "--language", "Go", "--license", "GPLv3", "--owner", "jillvalentine"}, + args: []string{"search", "--license", "GPLv3", "--owner", "jillvalentine"}, managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { em.ListFunc = func() []extensions.Extension { return []extensions.Extension{} @@ -201,7 +201,7 @@ func TestNewCmdExtension(t *testing.T) { values := url.Values{ "page": []string{"1"}, "per_page": []string{"30"}, - "q": []string{"language:Go license:GPLv3 topic:gh-extension user:jillvalentine"}, + "q": []string{"license:GPLv3 topic:gh-extension user:jillvalentine"}, } results := searchResults() results.Total = 1 @@ -211,7 +211,7 @@ func TestNewCmdExtension(t *testing.T) { httpmock.JSONResponse(results), ) }, - wantStdout: "screensaver\tvilmibm/gh-screensaver\t\t\tterminal animations\n", + wantStdout: "\tvilmibm/gh-screensaver\tterminal animations\n", }, { name: "search for extensions with web mode", From e84383589e2f181440d05e1925505ffb17639e84 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 8 Nov 2022 11:15:39 -0800 Subject: [PATCH 3/5] add more usage info --- pkg/cmd/extension/command.go | 37 +++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 552a9d1a5..98ab4876c 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -59,10 +59,23 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "search []", - Short: "Search available gh extensions", + Short: "Search through available gh extensions", Long: heredoc.Doc(` Search for gh extensions. + With no arguments, this command prints out the first 30 available + extensions sorted by number of stars. More extensions can be fetched + by specifying a higher limit with the --limit flag. + + When connected to a terminal, this command prints out three columns. + The first has a ✓ if the extension is installed locally. The second + is the full name of the extension repository in NAME/OWNER format. + The third is the extension's description. + + When not connected to a terminal, the ✓ character is rendered as the + word "installed" but otherwise the order and content of the columns + is the same. + This command behaves similarly to 'gh search repos' but does not support as many search qualifiers. For a finer grained search of extensions, try using: @@ -72,6 +85,28 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { and adding qualifiers as needed. See 'gh help search repos' to learn more about repository search. `), + Example: heredoc.Doc(` + # List the first 30 extensions sorted by star count, descending + $ gh ext search + + # List more extensions + $ gh ext search -L300 + + # List extensions matching the term "branch" + $ gh ext search branch + + # List extensions owned by organization "github" + $ gh ext search --owner github + + # List extensions, sorting by recently updated, ascending + $ gh ext search --sort updated --order asc + + # List extensions, filtering by license + $ gh ext search --license MIT + + # Open search results in the browser + $ gh ext search -w + `), RunE: func(cmd *cobra.Command, args []string) error { cfg, err := config() if err != nil { From 85e336526daef520bbbe82c38fc05bab1feb3e28 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 8 Nov 2022 12:31:33 -0800 Subject: [PATCH 4/5] tweak usage --- pkg/cmd/extension/command.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 98ab4876c..54d57c778 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -59,18 +59,18 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "search []", - Short: "Search through available gh extensions", + Short: "Search through extensions to the GitHub CLI", Long: heredoc.Doc(` Search for gh extensions. - With no arguments, this command prints out the first 30 available - extensions sorted by number of stars. More extensions can be fetched - by specifying a higher limit with the --limit flag. + With no arguments, this command prints out the first 30 extensions + available to install sorted by number of stars. More extensions can + be fetched by specifying a higher limit with the --limit flag. When connected to a terminal, this command prints out three columns. - The first has a ✓ if the extension is installed locally. The second - is the full name of the extension repository in NAME/OWNER format. - The third is the extension's description. + The first has a ✓ if the extension is already installed locally. The + second is the full name of the extension repository in NAME/OWNER + format. The third is the extension's description. When not connected to a terminal, the ✓ character is rendered as the word "installed" but otherwise the order and content of the columns @@ -84,6 +84,11 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { and adding qualifiers as needed. See 'gh help search repos' to learn more about repository search. + + For listing just the extensions that are already installed locally, + see: + + gh ext list `), Example: heredoc.Doc(` # List the first 30 extensions sorted by star count, descending From 3abba558f317acee75eef53b9bcbbc2d1179f603 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 8 Nov 2022 12:32:44 -0800 Subject: [PATCH 5/5] review feedback --- pkg/cmd/extension/command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index 54d57c778..1ea3b5530 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -59,7 +59,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "search []", - Short: "Search through extensions to the GitHub CLI", + Short: "Search extensions to the GitHub CLI", Long: heredoc.Doc(` Search for gh extensions. @@ -95,7 +95,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { $ gh ext search # List more extensions - $ gh ext search -L300 + $ gh ext search --limit 300 # List extensions matching the term "branch" $ gh ext search branch