From 2345f49ccbd6f1e902b9eae305d93494db30ec38 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 10 Sep 2020 20:58:06 -0500 Subject: [PATCH 01/12] support auth login --web --- pkg/cmd/auth/login/login.go | 116 ++++++++++++++++++------------- pkg/cmd/auth/login/login_test.go | 49 +++++++++++-- 2 files changed, 113 insertions(+), 52 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 630677dff..10a79c685 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -24,8 +24,11 @@ type LoginOptions struct { IO *iostreams.IOStreams Config func() (config.Config, error) + Interactive bool + Hostname string Token string + Web bool } func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command { @@ -51,6 +54,9 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm $ gh auth login # => do an interactive setup + $ gh auth login --web + # => open a browser to authenticate and do a non-interactive setup + $ gh auth login --with-token < mytoken.txt # => read token from mytoken.txt and authenticate against github.com @@ -58,6 +64,14 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm # => read token from mytoken.txt and authenticate against a GitHub Enterprise Server instance `), RunE: func(cmd *cobra.Command, args []string) error { + if !opts.IO.CanPrompt() && !(tokenStdin || opts.Web) { + return &cmdutil.FlagError{Err: errors.New("--web or --with-token required when not running interactively")} + } + + if tokenStdin && opts.Web { + return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --with-token")} + } + if tokenStdin { defer opts.IO.In.Close() token, err := ioutil.ReadAll(opts.IO.In) @@ -67,15 +81,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm opts.Token = strings.TrimSpace(string(token)) } - if opts.Token != "" { - // Assume non-interactive if a token is specified - if opts.Hostname == "" { - opts.Hostname = ghinstance.Default() - } - } else { - if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("--with-token required when not running interactively")} - } + if opts.IO.CanPrompt() && opts.Token == "" && !opts.Web { + opts.Interactive = true } if cmd.Flags().Changed("hostname") { @@ -84,6 +91,12 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm } } + if !opts.Interactive { + if opts.Hostname == "" { + opts.Hostname = ghinstance.Default() + } + } + if runF != nil { return runF(opts) } @@ -94,6 +107,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to authenticate with") cmd.Flags().BoolVar(&tokenStdin, "with-token", false, "Read token from standard input") + cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a browser to authenticate") return cmd } @@ -168,24 +182,26 @@ func loginRun(opts *LoginOptions) error { return err } - username, err := api.CurrentLoginName(apiClient, hostname) - if err != nil { - return fmt.Errorf("error using api: %w", err) - } - var keepGoing bool - err = prompt.SurveyAskOne(&survey.Confirm{ - Message: fmt.Sprintf( - "You're already logged into %s as %s. Do you want to re-authenticate?", - hostname, - username), - Default: false, - }, &keepGoing) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } + if opts.Interactive { + username, err := api.CurrentLoginName(apiClient, hostname) + if err != nil { + return fmt.Errorf("error using api: %w", err) + } + var keepGoing bool + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: fmt.Sprintf( + "You're already logged into %s as %s. Do you want to re-authenticate?", + hostname, + username), + Default: false, + }, &keepGoing) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } - if !keepGoing { - return nil + if !keepGoing { + return nil + } } } } @@ -195,15 +211,19 @@ func loginRun(opts *LoginOptions) error { } var authMode int - err = prompt.SurveyAskOne(&survey.Select{ - Message: "How would you like to authenticate?", - Options: []string{ - "Login with a web browser", - "Paste an authentication token", - }, - }, &authMode) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) + if opts.Web { + authMode = 0 + } else { + err = prompt.SurveyAskOne(&survey.Select{ + Message: "How would you like to authenticate?", + Options: []string{ + "Login with a web browser", + "Paste an authentication token", + }, + }, &authMode) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } } if authMode == 0 { @@ -239,19 +259,21 @@ func loginRun(opts *LoginOptions) error { } } - var gitProtocol string - err = prompt.SurveyAskOne(&survey.Select{ - Message: "Choose default git protocol", - Options: []string{ - "HTTPS", - "SSH", - }, - }, &gitProtocol) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } + gitProtocol := "https" + if opts.Interactive { + err = prompt.SurveyAskOne(&survey.Select{ + Message: "Choose default git protocol", + Options: []string{ + "HTTPS", + "SSH", + }, + }, &gitProtocol) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } - gitProtocol = strings.ToLower(gitProtocol) + gitProtocol = strings.ToLower(gitProtocol) + } fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) err = cfg.Set(hostname, "git_protocol", gitProtocol) diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/cmd/auth/login/login_test.go index 5d0ba1942..7166eac92 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/cmd/auth/login/login_test.go @@ -81,8 +81,9 @@ func Test_NewCmdLogin(t *testing.T) { stdinTTY: true, cli: "--hostname barry.burton", wants: LoginOptions{ - Hostname: "barry.burton", - Token: "", + Hostname: "barry.burton", + Token: "", + Interactive: true, }, }, { @@ -90,10 +91,33 @@ func Test_NewCmdLogin(t *testing.T) { stdinTTY: true, cli: "", wants: LoginOptions{ - Hostname: "", - Token: "", + Hostname: "", + Token: "", + Interactive: true, }, }, + { + name: "tty web", + stdinTTY: true, + cli: "--web", + wants: LoginOptions{ + Hostname: "github.com", + Web: true, + }, + }, + { + name: "nontty web", + cli: "--web", + wants: LoginOptions{ + Hostname: "github.com", + Web: true, + }, + }, + { + name: "web and with-token", + cli: "--web --with-token", + wantsErr: true, + }, } for _, tt := range tests { @@ -134,6 +158,8 @@ func Test_NewCmdLogin(t *testing.T) { assert.Equal(t, tt.wants.Token, gotOpts.Token) assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) + assert.Equal(t, tt.wants.Web, gotOpts.Web) + assert.Equal(t, tt.wants.Interactive, gotOpts.Interactive) }) } } @@ -262,6 +288,9 @@ func Test_loginRun_Survey(t *testing.T) { }{ { name: "already authenticated", + opts: &LoginOptions{ + Interactive: true, + }, cfg: func(cfg config.Config) { _ = cfg.Set("github.com", "oauth_token", "ghi789") }, @@ -280,7 +309,8 @@ func Test_loginRun_Survey(t *testing.T) { { name: "hostname set", opts: &LoginOptions{ - Hostname: "rebecca.chambers", + Hostname: "rebecca.chambers", + Interactive: true, }, wantHosts: "rebecca.chambers:\n oauth_token: def456\n git_protocol: https\n user: jillv\n", askStubs: func(as *prompt.AskStubber) { @@ -298,6 +328,9 @@ func Test_loginRun_Survey(t *testing.T) { { name: "choose enterprise", wantHosts: "brad.vickers:\n oauth_token: def456\n git_protocol: https\n user: jillv\n", + opts: &LoginOptions{ + Interactive: true, + }, askStubs: func(as *prompt.AskStubber) { as.StubOne(1) // host type enterprise as.StubOne("brad.vickers") // hostname @@ -315,6 +348,9 @@ func Test_loginRun_Survey(t *testing.T) { { name: "choose github.com", wantHosts: "github.com:\n oauth_token: def456\n git_protocol: https\n user: jillv\n", + opts: &LoginOptions{ + Interactive: true, + }, askStubs: func(as *prompt.AskStubber) { as.StubOne(0) // host type github.com as.StubOne(1) // auth mode: token @@ -325,6 +361,9 @@ func Test_loginRun_Survey(t *testing.T) { { name: "sets git_protocol", wantHosts: "github.com:\n oauth_token: def456\n git_protocol: ssh\n user: jillv\n", + opts: &LoginOptions{ + Interactive: true, + }, askStubs: func(as *prompt.AskStubber) { as.StubOne(0) // host type github.com as.StubOne(1) // auth mode: token From 662f83fcb916f52d448abaadc9582ce5b91151c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 14 Sep 2020 15:31:31 +0200 Subject: [PATCH 02/12] Fix `release download` Only ever try to set an `Authorization` request header to hosts that we have OAuth credentials for; skip the header otherwise. --- pkg/cmd/factory/http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/factory/http.go b/pkg/cmd/factory/http.go index 0adc87a12..47fbbefe8 100644 --- a/pkg/cmd/factory/http.go +++ b/pkg/cmd/factory/http.go @@ -24,7 +24,7 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", appVersion)), api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) { hostname := ghinstance.NormalizeHostname(req.URL.Hostname()) - if token, err := cfg.Get(hostname, "oauth_token"); err == nil || token != "" { + if token, err := cfg.Get(hostname, "oauth_token"); err == nil && token != "" { return fmt.Sprintf("token %s", token), nil } return "", nil From aa7246311437facd235e6f1610632c89b02b3bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 14 Sep 2020 16:25:30 +0200 Subject: [PATCH 03/12] Extract `environment` as a separate help topic Co-authored-by: Sam Coe --- pkg/cmd/root/help.go | 5 +++++ pkg/cmd/root/help_topic.go | 41 ++++++++++++++++++++++++++++++++++++++ pkg/cmd/root/root.go | 26 +++--------------------- 3 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 pkg/cmd/root/help_topic.go diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index 019ec53cd..b2841ba9f 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -78,6 +78,11 @@ func rootHelpFunc(command *cobra.Command, args []string) { return } + if helpTopic := command.Annotations["helpTopic"]; helpTopic == "true" { + fmt.Fprint(command.OutOrStdout(), command.Long) + return + } + coreCommands := []string{} additionalCommands := []string{} for _, c := range command.Commands() { diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go new file mode 100644 index 000000000..8e17685ae --- /dev/null +++ b/pkg/cmd/root/help_topic.go @@ -0,0 +1,41 @@ +package root + +import ( + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +func NewHelpTopic(topic string) *cobra.Command { + return &cobra.Command{ + Use: "environment", + Long: heredoc.Doc(` + GITHUB_TOKEN: an authentication token for github.com API requests. Setting this avoids + being prompted to authenticate and takes precedence over previously stored credentials. + + GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise. + + GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands + that otherwise operate on a local repository. + + GH_HOST: specify the GitHub hostname for commands that would otherwise assume + the "github.com" host when not in a context of an existing repository. + + GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use + for authoring text. + + BROWSER: the web browser to use for opening links. + + DEBUG: set to any value to enable verbose output to standard error. Include values "api" + or "oauth" to print detailed information about HTTP requests or authentication flow. + + GLAMOUR_STYLE: the style to use for rendering Markdown. See + https://github.com/charmbracelet/glamour#styles + + NO_COLOR: avoid printing ANSI escape sequences for color output. + `), + Hidden: true, + Annotations: map[string]string{ + "helpTopic": "true", + }, + } +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 0eadbd575..2c597352e 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -44,29 +44,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { Open an issue using “gh issue create -R cli/cli” `), "help:environment": heredoc.Doc(` - GITHUB_TOKEN: an authentication token for github.com API requests. Setting this avoids - being prompted to authenticate and takes precedence over previously stored credentials. - - GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise. - - GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands - that otherwise operate on a local repository. - - GH_HOST: specify the GitHub hostname for commands that would otherwise assume - the "github.com" host when not in a context of an existing repository. - - GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use - for authoring text. - - BROWSER: the web browser to use for opening links. - - DEBUG: set to any value to enable verbose output to standard error. Include values "api" - or "oauth" to print detailed information about HTTP requests or authentication flow. - - GLAMOUR_STYLE: the style to use for rendering Markdown. See - https://github.com/charmbracelet/glamour#styles - - NO_COLOR: avoid printing ANSI escape sequences for color output. + See 'gh help environment' for the list of supported environment variables. `), }, } @@ -113,6 +91,8 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(gistCmd.NewCmdGist(f)) cmd.AddCommand(NewCmdCompletion(f.IOStreams)) + cmd.AddCommand(NewHelpTopic("environment")) + // the `api` command should not inherit any extra HTTP headers bareHTTPCmdFactory := *f bareHTTPCmdFactory.HttpClient = func() (*http.Client, error) { From 3c32507a1316ad38e82374a8bfa8cc126e5bc312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 14 Sep 2020 16:26:23 +0200 Subject: [PATCH 04/12] Consistent use of quotes to delineate commands to be run --- pkg/cmd/root/help.go | 2 +- pkg/cmd/root/root.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index b2841ba9f..4f9a798fb 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -144,7 +144,7 @@ func rootHelpFunc(command *cobra.Command, args []string) { helpEntries = append(helpEntries, helpEntry{"ENVIRONMENT VARIABLES", command.Annotations["help:environment"]}) } helpEntries = append(helpEntries, helpEntry{"LEARN MORE", ` -Use "gh --help" for more information about a command. +Use 'gh --help' for more information about a command. Read the manual at https://cli.github.com/manual`}) if _, ok := command.Annotations["help:feedback"]; ok { helpEntries = append(helpEntries, helpEntry{"FEEDBACK", command.Annotations["help:feedback"]}) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 2c597352e..f3319e456 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -41,7 +41,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { `), Annotations: map[string]string{ "help:feedback": heredoc.Doc(` - Open an issue using “gh issue create -R cli/cli” + Open an issue using 'gh issue create -R cli/cli' `), "help:environment": heredoc.Doc(` See 'gh help environment' for the list of supported environment variables. From 68a019cc3c9bae5609b29d6ff437624707ff5631 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 14 Sep 2020 10:16:50 -0500 Subject: [PATCH 05/12] review feedback --- pkg/cmd/auth/login/login.go | 55 +++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 10a79c685..b6d2a2f99 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -54,9 +54,6 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm $ gh auth login # => do an interactive setup - $ gh auth login --web - # => open a browser to authenticate and do a non-interactive setup - $ gh auth login --with-token < mytoken.txt # => read token from mytoken.txt and authenticate against github.com @@ -174,7 +171,7 @@ func loginRun(opts *LoginOptions) error { existingToken, _ := cfg.Get(hostname, "oauth_token") - if existingToken != "" { + if existingToken != "" && opts.Interactive { err := client.ValidateHostCfg(hostname, cfg) if err == nil { apiClient, err := client.ClientFromCfg(hostname, cfg) @@ -182,26 +179,24 @@ func loginRun(opts *LoginOptions) error { return err } - if opts.Interactive { - username, err := api.CurrentLoginName(apiClient, hostname) - if err != nil { - return fmt.Errorf("error using api: %w", err) - } - var keepGoing bool - err = prompt.SurveyAskOne(&survey.Confirm{ - Message: fmt.Sprintf( - "You're already logged into %s as %s. Do you want to re-authenticate?", - hostname, - username), - Default: false, - }, &keepGoing) - if err != nil { - return fmt.Errorf("could not prompt: %w", err) - } + username, err := api.CurrentLoginName(apiClient, hostname) + if err != nil { + return fmt.Errorf("error using api: %w", err) + } + var keepGoing bool + err = prompt.SurveyAskOne(&survey.Confirm{ + Message: fmt.Sprintf( + "You're already logged into %s as %s. Do you want to re-authenticate?", + hostname, + username), + Default: false, + }, &keepGoing) + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } - if !keepGoing { - return nil - } + if !keepGoing { + return nil } } } @@ -273,15 +268,15 @@ func loginRun(opts *LoginOptions) error { } gitProtocol = strings.ToLower(gitProtocol) - } - fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) - err = cfg.Set(hostname, "git_protocol", gitProtocol) - if err != nil { - return err - } + fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) + err = cfg.Set(hostname, "git_protocol", gitProtocol) + if err != nil { + return err + } - fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", utils.GreenCheck()) + fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", utils.GreenCheck()) + } apiClient, err := client.ClientFromCfg(hostname, cfg) if err != nil { From 21449213e52b07f341e2b42d3fec9b6d144fcceb Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 14 Sep 2020 17:50:50 +0200 Subject: [PATCH 06/12] Expand help topic functionality --- pkg/cmd/root/help.go | 5 --- pkg/cmd/root/help_topic.go | 79 +++++++++++++++++++++++++++----------- pkg/cmd/root/root.go | 4 +- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index 4f9a798fb..400be3834 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -78,11 +78,6 @@ func rootHelpFunc(command *cobra.Command, args []string) { return } - if helpTopic := command.Annotations["helpTopic"]; helpTopic == "true" { - fmt.Fprint(command.OutOrStdout(), command.Long) - return - } - coreCommands := []string{} additionalCommands := []string{} for _, c := range command.Commands() { diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 8e17685ae..8a4e6f931 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -1,41 +1,76 @@ package root import ( + "fmt" + "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" ) func NewHelpTopic(topic string) *cobra.Command { - return &cobra.Command{ - Use: "environment", - Long: heredoc.Doc(` - GITHUB_TOKEN: an authentication token for github.com API requests. Setting this avoids - being prompted to authenticate and takes precedence over previously stored credentials. + topicContent := make(map[string]string) - GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise. + topicContent["environment"] = heredoc.Doc(` + GITHUB_TOKEN: an authentication token for github.com API requests. Setting this avoids + being prompted to authenticate and takes precedence over previously stored credentials. - GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands - that otherwise operate on a local repository. + GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise. - GH_HOST: specify the GitHub hostname for commands that would otherwise assume - the "github.com" host when not in a context of an existing repository. + GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands + that otherwise operate on a local repository. - GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use - for authoring text. + GH_HOST: specify the GitHub hostname for commands that would otherwise assume + the "github.com" host when not in a context of an existing repository. - BROWSER: the web browser to use for opening links. + GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use + for authoring text. - DEBUG: set to any value to enable verbose output to standard error. Include values "api" - or "oauth" to print detailed information about HTTP requests or authentication flow. + BROWSER: the web browser to use for opening links. - GLAMOUR_STYLE: the style to use for rendering Markdown. See - https://github.com/charmbracelet/glamour#styles + DEBUG: set to any value to enable verbose output to standard error. Include values "api" + or "oauth" to print detailed information about HTTP requests or authentication flow. - NO_COLOR: avoid printing ANSI escape sequences for color output. - `), + GLAMOUR_STYLE: the style to use for rendering Markdown. See + https://github.com/charmbracelet/glamour#styles + + NO_COLOR: avoid printing ANSI escape sequences for color output. + `) + + cmd := &cobra.Command{ + Use: topic, + Long: topicContent[topic], Hidden: true, - Annotations: map[string]string{ - "helpTopic": "true", - }, + Args: cobra.NoArgs, } + + cmd.SetHelpFunc(helpTopicHelpFunc) + cmd.SetUsageFunc(helpTopicUsageFunc) + + return cmd +} + +func helpTopicHelpFunc(command *cobra.Command, args []string) { + if len(args) >= 2 && args[1] != "--help" && args[1] != "-h" { + command.Printf("unknown command %q for %q\n", args[1], command.CommandPath()) + + if args[1] == "help" { + command.Print("\nDid you mean this?\n") + command.Printf("\t%s\n\n", "--help") + } else { + command.Printf("\n") + } + + helpTopicUsageFunc(command) + command.Printf("\n") + hasFailed = true + return + } + + fmt.Fprint(command.OutOrStdout(), command.Long) +} + +func helpTopicUsageFunc(command *cobra.Command) error { + command.Printf("Usage: gh help %s", command.Use) + + return nil } diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index f3319e456..7ad31fb68 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -82,8 +82,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmdutil.DisableAuthCheck(cmd) - // CHILD COMMANDS - + // Child commands cmd.AddCommand(aliasCmd.NewCmdAlias(f)) cmd.AddCommand(authCmd.NewCmdAuth(f)) cmd.AddCommand(configCmd.NewCmdConfig(f)) @@ -91,6 +90,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(gistCmd.NewCmdGist(f)) cmd.AddCommand(NewCmdCompletion(f.IOStreams)) + // Help Topics cmd.AddCommand(NewHelpTopic("environment")) // the `api` command should not inherit any extra HTTP headers From c3863e35b4b72adcefa71ae82160d3ed32459b58 Mon Sep 17 00:00:00 2001 From: Amanda Pinsker Date: Mon, 14 Sep 2020 12:40:56 -0400 Subject: [PATCH 07/12] Add prompt to gh config help --- pkg/cmd/config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 3fa86ddcb..6dd0f528c 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -18,6 +18,7 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { Current respected settings: - git_protocol: "https" or "ssh". Default is "https". - editor: if unset, defaults to environment variables. + - prompt: "enabled" or "disabled". Toggles interactive prompting. `), } From db77f2dc66c5ece25ea96b36dffb5c475ec52771 Mon Sep 17 00:00:00 2001 From: Amanda Pinsker Date: Mon, 14 Sep 2020 12:42:13 -0400 Subject: [PATCH 08/12] Add examples --- pkg/cmd/config/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 6dd0f528c..e5e9d170a 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -73,6 +73,8 @@ func NewCmdConfigSet(f *cmdutil.Factory) *cobra.Command { Example: heredoc.Doc(` $ gh config set editor vim $ gh config set editor "code --wait" + $ gh config set git_protocol ssh + $ gh config set prompt disabled `), Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { From 7ecb6a413fe9663614f46b6837ca71012762de47 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 14 Sep 2020 22:21:05 +0200 Subject: [PATCH 09/12] Add tests for help topics --- pkg/cmd/root/help_topic.go | 24 ++-------- pkg/cmd/root/help_topic_test.go | 79 +++++++++++++++++++++++++++++++++ pkg/cmd/root/root.go | 2 +- 3 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 pkg/cmd/root/help_topic_test.go diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go index 8a4e6f931..1357825a2 100644 --- a/pkg/cmd/root/help_topic.go +++ b/pkg/cmd/root/help_topic.go @@ -1,8 +1,6 @@ package root import ( - "fmt" - "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" ) @@ -41,6 +39,7 @@ func NewHelpTopic(topic string) *cobra.Command { Long: topicContent[topic], Hidden: true, Args: cobra.NoArgs, + Run: helpTopicHelpFunc, } cmd.SetHelpFunc(helpTopicHelpFunc) @@ -50,27 +49,10 @@ func NewHelpTopic(topic string) *cobra.Command { } func helpTopicHelpFunc(command *cobra.Command, args []string) { - if len(args) >= 2 && args[1] != "--help" && args[1] != "-h" { - command.Printf("unknown command %q for %q\n", args[1], command.CommandPath()) - - if args[1] == "help" { - command.Print("\nDid you mean this?\n") - command.Printf("\t%s\n\n", "--help") - } else { - command.Printf("\n") - } - - helpTopicUsageFunc(command) - command.Printf("\n") - hasFailed = true - return - } - - fmt.Fprint(command.OutOrStdout(), command.Long) + command.Print(command.Long) } func helpTopicUsageFunc(command *cobra.Command) error { - command.Printf("Usage: gh help %s", command.Use) - + command.Printf("Usage: gh help %s", command.Use) return nil } diff --git a/pkg/cmd/root/help_topic_test.go b/pkg/cmd/root/help_topic_test.go new file mode 100644 index 000000000..f194541ac --- /dev/null +++ b/pkg/cmd/root/help_topic_test.go @@ -0,0 +1,79 @@ +package root + +import ( + "testing" + + "github.com/cli/cli/pkg/iostreams" + "github.com/stretchr/testify/assert" +) + +func TestNewHelpTopic(t *testing.T) { + tests := []struct { + name string + topic string + args []string + flags []string + wantsErr bool + }{ + { + name: "valid topic", + topic: "environment", + args: []string{}, + flags: []string{}, + wantsErr: false, + }, + { + name: "invalid topic", + topic: "invalid", + args: []string{}, + flags: []string{}, + wantsErr: false, + }, + { + name: "more than zero args", + topic: "environment", + args: []string{"invalid"}, + flags: []string{}, + wantsErr: true, + }, + { + name: "more than zero flags", + topic: "environment", + args: []string{}, + flags: []string{"--invalid"}, + wantsErr: true, + }, + { + name: "help arg", + topic: "environment", + args: []string{"help"}, + flags: []string{}, + wantsErr: true, + }, + { + name: "help flag", + topic: "environment", + args: []string{}, + flags: []string{"--help"}, + wantsErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, stdout, stderr := iostreams.Test() + + cmd := NewHelpTopic(tt.topic) + cmd.SetArgs(append(tt.args, tt.flags...)) + cmd.SetOut(stdout) + cmd.SetErr(stderr) + + _, err := cmd.ExecuteC() + if tt.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + }) + } +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 7ad31fb68..e3257ed09 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -90,7 +90,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(gistCmd.NewCmdGist(f)) cmd.AddCommand(NewCmdCompletion(f.IOStreams)) - // Help Topics + // Help topics cmd.AddCommand(NewHelpTopic("environment")) // the `api` command should not inherit any extra HTTP headers From 3d350e8707450d06036841c05223375002b03cd1 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez <6853656+cristiand391@users.noreply.github.com> Date: Mon, 14 Sep 2020 20:31:22 +0000 Subject: [PATCH 10/12] Go 1.15+ is required to run the test suit --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8a1dc4849..09f60f318 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -27,7 +27,7 @@ Please avoid: Prerequisites: - Go 1.13+ for building the binary -- Go 1.14+ for running the test suite +- Go 1.15+ for running the test suite Build with: `make` or `go build -o bin/gh ./cmd/gh` From 9f486efbc64ea338071fbf3633c74437f92bf024 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 15 Sep 2020 09:59:27 -0500 Subject: [PATCH 11/12] hackday: gh repo garden (#1049) * add gh repo garden * move file * oops * fixes * fix clearing * block windows sadly * broken wip * fix api thing * do not add to client * move helper as it does not work on windows * hide command * macos fix * Update pkg/cmd/repo/garden/garden.go Co-authored-by: Lee Reilly * default for key input loop * get redrawing working * clean up garden update, it all works * notes * fix arrow keys and just do wads/arrows/vi * this function is only called once now * support ghes * add a progress indicator * cap maxCommits Co-authored-by: Lee Reilly --- pkg/cmd/repo/garden/garden.go | 480 ++++++++++++++++++++++++++++++++++ pkg/cmd/repo/garden/http.go | 105 ++++++++ pkg/cmd/repo/repo.go | 2 + 3 files changed, 587 insertions(+) create mode 100644 pkg/cmd/repo/garden/garden.go create mode 100644 pkg/cmd/repo/garden/http.go diff --git a/pkg/cmd/repo/garden/garden.go b/pkg/cmd/repo/garden/garden.go new file mode 100644 index 000000000..46761b253 --- /dev/null +++ b/pkg/cmd/repo/garden/garden.go @@ -0,0 +1,480 @@ +package garden + +import ( + "bytes" + "errors" + "fmt" + "io" + "math/rand" + "net/http" + "os/exec" + "runtime" + "strconv" + "strings" + + "github.com/cli/cli/api" + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/utils" + "github.com/spf13/cobra" +) + +type Geometry struct { + Width int + Height int + Density float64 + Repository ghrepo.Interface +} + +type Player struct { + X int + Y int + Char string + Geo *Geometry + ShoeMoistureContent int +} + +type Commit struct { + Email string + Handle string + Sha string + Char string +} + +type Cell struct { + Char string + StatusLine string +} + +const ( + DirUp = iota + DirDown + DirLeft + DirRight +) + +type Direction = int + +func (p *Player) move(direction Direction) bool { + switch direction { + case DirUp: + if p.Y == 0 { + return false + } + p.Y-- + case DirDown: + if p.Y == p.Geo.Height-1 { + return false + } + p.Y++ + case DirLeft: + if p.X == 0 { + return false + } + p.X-- + case DirRight: + if p.X == p.Geo.Width-1 { + return false + } + p.X++ + } + + return true +} + +type GardenOptions struct { + HttpClient func() (*http.Client, error) + IO *iostreams.IOStreams + BaseRepo func() (ghrepo.Interface, error) + + RepoArg string +} + +func NewCmdGarden(f *cmdutil.Factory, runF func(*GardenOptions) error) *cobra.Command { + opts := GardenOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + BaseRepo: f.BaseRepo, + } + + cmd := &cobra.Command{ + Use: "garden []", + Short: "Explore a git repository as a garden", + Long: "Use arrow keys, WASD or vi keys to move. q to quit.", + Hidden: true, + RunE: func(c *cobra.Command, args []string) error { + if len(args) > 0 { + opts.RepoArg = args[0] + } + if runF != nil { + return runF(&opts) + } + return gardenRun(&opts) + }, + } + + return cmd +} + +func gardenRun(opts *GardenOptions) error { + out := opts.IO.Out + + if runtime.GOOS == "windows" { + return errors.New("sorry :( this command only works on linux and macos") + } + + if !opts.IO.IsStdoutTTY() { + return errors.New("must be connected to a terminal") + } + + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + var toView ghrepo.Interface + apiClient := api.NewClientFromHTTP(httpClient) + if opts.RepoArg == "" { + var err error + toView, err = opts.BaseRepo() + if err != nil { + return err + } + } else { + var err error + viewURL := opts.RepoArg + if !strings.Contains(viewURL, "/") { + currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default()) + if err != nil { + return err + } + viewURL = currentUser + "/" + viewURL + } + toView, err = ghrepo.FromFullName(viewURL) + if err != nil { + return fmt.Errorf("argument error: %w", err) + } + } + + seed := computeSeed(ghrepo.FullName(toView)) + rand.Seed(seed) + + termWidth, termHeight, err := utils.TerminalSize(out) + if err != nil { + return err + } + + termWidth -= 10 + termHeight -= 10 + + geo := &Geometry{ + Width: termWidth, + Height: termHeight, + Repository: toView, + // TODO based on number of commits/cells instead of just hardcoding + Density: 0.3, + } + + maxCommits := (geo.Width * geo.Height) / 2 + + opts.IO.StartProgressIndicator() + fmt.Fprintln(out, "gathering commits; this could take a minute...") + commits, err := getCommits(httpClient, toView, maxCommits) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } + player := &Player{0, 0, utils.Bold("@"), geo, 0} + + garden := plantGarden(commits, geo) + clear(opts.IO) + drawGarden(out, garden, player) + + // thanks stackoverflow https://stackoverflow.com/a/17278776 + if runtime.GOOS == "darwin" { + _ = exec.Command("stty", "-f", "/dev/tty", "cbreak", "min", "1").Run() + _ = exec.Command("stty", "-f", "/dev/tty", "-echo").Run() + } else { + _ = exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run() + _ = exec.Command("stty", "-F", "/dev/tty", "-echo").Run() + } + + var b []byte = make([]byte, 3) + for { + _, _ = opts.IO.In.Read(b) + + oldX := player.X + oldY := player.Y + moved := false + quitting := false + continuing := false + + switch { + case isLeft(b): + moved = player.move(DirLeft) + case isRight(b): + moved = player.move(DirRight) + case isUp(b): + moved = player.move(DirUp) + case isDown(b): + moved = player.move(DirDown) + case isQuit(b): + quitting = true + default: + continuing = true + } + + if quitting { + break + } + + if !moved || continuing { + continue + } + + underPlayer := garden[player.Y][player.X] + previousCell := garden[oldY][oldX] + + // print whatever was just under player + + fmt.Fprint(out, "\033[;H") // move to top left + for x := 0; x < oldX && x < player.Geo.Width; x++ { + fmt.Fprint(out, "\033[C") + } + for y := 0; y < oldY && y < player.Geo.Height; y++ { + fmt.Fprint(out, "\033[B") + } + fmt.Fprint(out, previousCell.Char) + + // print player character + fmt.Fprint(out, "\033[;H") // move to top left + for x := 0; x < player.X && x < player.Geo.Width; x++ { + fmt.Fprint(out, "\033[C") + } + for y := 0; y < player.Y && y < player.Geo.Height; y++ { + fmt.Fprint(out, "\033[B") + } + fmt.Fprint(out, player.Char) + + // handle stream wettening + + if strings.Contains(underPlayer.StatusLine, "stream") { + player.ShoeMoistureContent = 5 + } else { + if player.ShoeMoistureContent > 0 { + player.ShoeMoistureContent-- + } + } + + // status line stuff + sl := statusLine(garden, player) + + fmt.Fprint(out, "\033[;H") // move to top left + for y := 0; y < player.Geo.Height-1; y++ { + fmt.Fprint(out, "\033[B") + } + fmt.Fprintln(out) + fmt.Fprintln(out) + + fmt.Fprint(out, utils.Bold(sl)) + } + + clear(opts.IO) + fmt.Fprint(out, "\033[?25h") + fmt.Fprintln(out) + fmt.Fprintln(out, utils.Bold("You turn and walk away from the wildflower garden...")) + + return nil +} + +func isLeft(b []byte) bool { + left := []byte{27, 91, 68} + r := rune(b[0]) + return bytes.EqualFold(b, left) || r == 'a' || r == 'h' +} + +func isRight(b []byte) bool { + right := []byte{27, 91, 67} + r := rune(b[0]) + return bytes.EqualFold(b, right) || r == 'd' || r == 'l' +} + +func isDown(b []byte) bool { + down := []byte{27, 91, 66} + r := rune(b[0]) + return bytes.EqualFold(b, down) || r == 's' || r == 'j' +} + +func isUp(b []byte) bool { + up := []byte{27, 91, 65} + r := rune(b[0]) + return bytes.EqualFold(b, up) || r == 'w' || r == 'k' +} + +func isQuit(b []byte) bool { + return rune(b[0]) == 'q' +} + +func plantGarden(commits []*Commit, geo *Geometry) [][]*Cell { + cellIx := 0 + grassCell := &Cell{RGB(0, 200, 0, ","), "You're standing on a patch of grass in a field of wildflowers."} + garden := [][]*Cell{} + streamIx := rand.Intn(geo.Width - 1) + if streamIx == geo.Width/2 { + streamIx-- + } + tint := 0 + for y := 0; y < geo.Height; y++ { + if cellIx == len(commits)-1 { + break + } + garden = append(garden, []*Cell{}) + for x := 0; x < geo.Width; x++ { + if (y > 0 && (x == 0 || x == geo.Width-1)) || y == geo.Height-1 { + garden[y] = append(garden[y], &Cell{ + Char: RGB(0, 150, 0, "^"), + StatusLine: "You're standing under a tall, leafy tree.", + }) + continue + } + if x == streamIx { + garden[y] = append(garden[y], &Cell{ + Char: RGB(tint, tint, 255, "#"), + StatusLine: "You're standing in a shallow stream. It's refreshing.", + }) + tint += 15 + streamIx-- + if rand.Float64() < 0.5 { + streamIx++ + } + if streamIx < 0 { + streamIx = 0 + } + if streamIx > geo.Width { + streamIx = geo.Width + } + continue + } + if y == 0 && (x < geo.Width/2 || x > geo.Width/2) { + garden[y] = append(garden[y], &Cell{ + Char: RGB(0, 200, 0, ","), + StatusLine: "You're standing by a wildflower garden. There is a light breeze.", + }) + continue + } else if y == 0 && x == geo.Width/2 { + garden[y] = append(garden[y], &Cell{ + Char: RGB(139, 69, 19, "+"), + StatusLine: fmt.Sprintf("You're standing in front of a weather-beaten sign that says %s.", ghrepo.FullName(geo.Repository)), + }) + continue + } + + if cellIx == len(commits)-1 { + garden[y] = append(garden[y], grassCell) + continue + } + + chance := rand.Float64() + if chance <= geo.Density { + commit := commits[cellIx] + garden[y] = append(garden[y], &Cell{ + Char: commits[cellIx].Char, + StatusLine: fmt.Sprintf("You're standing at a flower called %s planted by %s.", commit.Sha[0:6], commit.Handle), + }) + cellIx++ + } else { + garden[y] = append(garden[y], grassCell) + } + } + } + + return garden +} + +func drawGarden(out io.Writer, garden [][]*Cell, player *Player) { + fmt.Fprint(out, "\033[?25l") // hide cursor. it needs to be restored at command exit. + sl := "" + for y, gardenRow := range garden { + for x, gardenCell := range gardenRow { + char := "" + underPlayer := (player.X == x && player.Y == y) + if underPlayer { + sl = gardenCell.StatusLine + char = utils.Bold(player.Char) + + if strings.Contains(gardenCell.StatusLine, "stream") { + player.ShoeMoistureContent = 5 + } + } else { + char = gardenCell.Char + } + + fmt.Fprint(out, char) + } + fmt.Fprintln(out) + } + + fmt.Println() + fmt.Fprintln(out, utils.Bold(sl)) +} + +func statusLine(garden [][]*Cell, player *Player) string { + statusLine := garden[player.Y][player.X].StatusLine + " " + if player.ShoeMoistureContent > 1 { + statusLine += "\nYour shoes squish with water from the stream." + } else if player.ShoeMoistureContent == 1 { + statusLine += "\nYour shoes seem to have dried out." + } else { + statusLine += "\n " + } + + return statusLine +} + +func shaToColorFunc(sha string) func(string) string { + return func(c string) string { + red, err := strconv.ParseInt(sha[0:2], 16, 64) + if err != nil { + panic(err) + } + + green, err := strconv.ParseInt(sha[2:4], 16, 64) + if err != nil { + panic(err) + } + + blue, err := strconv.ParseInt(sha[4:6], 16, 64) + if err != nil { + panic(err) + } + + return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", red, green, blue, c) + } +} + +func computeSeed(seed string) int64 { + lol := "" + + for _, r := range seed { + lol += fmt.Sprintf("%d", int(r)) + } + + result, err := strconv.ParseInt(lol[0:10], 10, 64) + if err != nil { + panic(err) + } + + return result +} + +func clear(io *iostreams.IOStreams) { + cmd := exec.Command("clear") + cmd.Stdout = io.Out + _ = cmd.Run() +} + +func RGB(r, g, b int, x string) string { + return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x) +} diff --git a/pkg/cmd/repo/garden/http.go b/pkg/cmd/repo/garden/http.go new file mode 100644 index 000000000..ff7f47fe0 --- /dev/null +++ b/pkg/cmd/repo/garden/http.go @@ -0,0 +1,105 @@ +package garden + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/internal/ghrepo" +) + +func getCommits(client *http.Client, repo ghrepo.Interface, maxCommits int) ([]*Commit, error) { + type Item struct { + Author struct { + Login string + } + Sha string + } + + type Result []Item + + commits := []*Commit{} + + pathF := func(page int) string { + return fmt.Sprintf("repos/%s/%s/commits?per_page=100&page=%d", repo.RepoOwner(), repo.RepoName(), page) + } + + page := 1 + paginating := true + for paginating { + if len(commits) >= maxCommits { + break + } + result := Result{} + resp, err := getResponse(client, pathF(page), &result) + if err != nil { + return nil, err + } + for _, r := range result { + colorFunc := shaToColorFunc(r.Sha) + handle := r.Author.Login + if handle == "" { + handle = "a mysterious stranger" + } + commits = append(commits, &Commit{ + Handle: handle, + Sha: r.Sha, + Char: colorFunc(string(handle[0])), + }) + } + link := resp.Header["Link"] + if !strings.Contains(link[0], "last") { + paginating = false + } + page++ + time.Sleep(500) + } + + // reverse to get older commits first + for i, j := 0, len(commits)-1; i < j; i, j = i+1, j-1 { + commits[i], commits[j] = commits[j], commits[i] + } + + return commits, nil +} + +func getResponse(client *http.Client, path string, data interface{}) (*http.Response, error) { + url := ghinstance.RESTPrefix(ghinstance.OverridableDefault()) + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + return nil, errors.New("api call failed") + } + + if resp.StatusCode == http.StatusNoContent { + return resp, nil + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, &data) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index 551c96641..02e6c368b 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -6,6 +6,7 @@ import ( repoCreateCmd "github.com/cli/cli/pkg/cmd/repo/create" creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits" repoForkCmd "github.com/cli/cli/pkg/cmd/repo/fork" + gardenCmd "github.com/cli/cli/pkg/cmd/repo/garden" repoViewCmd "github.com/cli/cli/pkg/cmd/repo/view" "github.com/cli/cli/pkg/cmdutil" "github.com/spf13/cobra" @@ -36,6 +37,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(repoCloneCmd.NewCmdClone(f, nil)) cmd.AddCommand(repoCreateCmd.NewCmdCreate(f, nil)) cmd.AddCommand(creditsCmd.NewCmdRepoCredits(f, nil)) + cmd.AddCommand(gardenCmd.NewCmdGarden(f, nil)) return cmd } From 50211eade351a2bc5832a360b874294c8dbdb3be Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Tue, 15 Sep 2020 12:00:07 -0500 Subject: [PATCH 12/12] add issue template for general feedback is this a good idea? --- .github/ISSUE_TEMPLATE/feedback.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feedback.md diff --git a/.github/ISSUE_TEMPLATE/feedback.md b/.github/ISSUE_TEMPLATE/feedback.md new file mode 100644 index 000000000..837c36632 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feedback.md @@ -0,0 +1,28 @@ +--- +name: "\U0001F4E3 Feedback" +about: Give us general feedback about the GitHub CLI +title: '' +labels: feedback +assignees: '' + +--- + +# CLI Feedback + +You can use this template to give us structured feedback or just wipe it and leave us a note. Thank you! + +## What have you loved? + +_eg "the nice colors"_ + +## What was confusing or gave you pause? + +_eg "it did something unexpected"_ + +## Are there features you'd like to see added? + +_eg "gh cli needs mini-games"_ + +## Anything else? + +_eg "have a nice day"_