From 6effcd4261f7574811e4a74e9381f1a9fd531543 Mon Sep 17 00:00:00 2001 From: AliabbasMerchant Date: Mon, 18 May 2020 00:37:28 +0530 Subject: [PATCH 01/59] Allow choosing a blank issue/pr template --- command/issue.go | 1 + command/pr_create.go | 1 + command/title_body_survey.go | 54 +++++++++++++++++++++++------------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/command/issue.go b/command/issue.go index 5eed13a13..af3a03fd3 100644 --- a/command/issue.go +++ b/command/issue.go @@ -421,6 +421,7 @@ func issueCreate(cmd *cobra.Command, args []string) error { action := SubmitAction tb := issueMetadataState{ + Type: issueMetadata, Assignees: assignees, Labels: labelNames, Projects: projectNames, diff --git a/command/pr_create.go b/command/pr_create.go index 96dc08791..1100bf9a0 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -203,6 +203,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { } tb := issueMetadataState{ + Type: prMetadata, Reviewers: reviewers, Assignees: assignees, Labels: labelNames, diff --git a/command/title_body_survey.go b/command/title_body_survey.go index 17f7af9fd..311c816ea 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -13,8 +13,16 @@ import ( ) type Action int +type metadataStateType int + +const ( + issueMetadata metadataStateType = 0 + prMetadata metadataStateType = 1 +) type issueMetadataState struct { + Type metadataStateType + Body string Title string Action Action @@ -99,30 +107,36 @@ func confirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) { } } -func selectTemplate(templatePaths []string) (string, error) { +func selectTemplate(templatePaths []string, metadataType metadataStateType) (string, error) { templateResponse := struct { Index int }{} - if len(templatePaths) > 1 { - templateNames := make([]string, 0, len(templatePaths)) - for _, p := range templatePaths { - templateNames = append(templateNames, githubtemplate.ExtractName(p)) - } - - selectQs := []*survey.Question{ - { - Name: "index", - Prompt: &survey.Select{ - Message: "Choose a template", - Options: templateNames, - }, - }, - } - if err := SurveyAsk(selectQs, &templateResponse); err != nil { - return "", fmt.Errorf("could not prompt: %w", err) - } + templateNames := make([]string, 0, len(templatePaths)) + for _, p := range templatePaths { + templateNames = append(templateNames, githubtemplate.ExtractName(p)) + } + if metadataType == issueMetadata { + templateNames = append(templateNames, "Open a blank issue") + } else if metadataType == prMetadata { + templateNames = append(templateNames, "Open a blank PR") } + selectQs := []*survey.Question{ + { + Name: "index", + Prompt: &survey.Select{ + Message: "Choose a template", + Options: templateNames, + }, + }, + } + if err := SurveyAsk(selectQs, &templateResponse); err != nil { + return "", fmt.Errorf("could not prompt: %w", err) + } + + if templateResponse.Index == len(templatePaths) { // the user has selected the blank template + return "", nil + } templateContents := githubtemplate.ExtractContents(templatePaths[templateResponse.Index]) return string(templateContents), nil } @@ -139,7 +153,7 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie if providedBody == "" { if len(templatePaths) > 0 { var err error - templateContents, err = selectTemplate(templatePaths) + templateContents, err = selectTemplate(templatePaths, issueState.Type) if err != nil { return err } From 01f272dead12bafcf2c48aad7546f7bd95fb8642 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 18 May 2020 11:44:21 -0500 Subject: [PATCH 02/59] give our pr template a name --- .github/PULL_REQUEST_TEMPLATE/bug_fix.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md index 5659ce40b..182edcc54 100644 --- a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +++ b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md @@ -1,3 +1,9 @@ +--- +name: "\U0001F41B Bug fix" +about: Fix a bug in GitHub CLI + +--- + ## Summary From 26987c14346e0fc4edee277a64012124c29d3819 Mon Sep 17 00:00:00 2001 From: AliabbasMerchant Date: Thu, 11 Jun 2020 17:27:18 +0530 Subject: [PATCH 43/59] Fix error when Milestone is empty during PR create --- command/title_body_survey.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/command/title_body_survey.go b/command/title_body_survey.go index d6fe11bbf..1dcf3c668 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -371,10 +371,8 @@ func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClie issueState.Assignees = values.Assignees issueState.Labels = values.Labels issueState.Projects = values.Projects - issueState.Milestones = []string{values.Milestone} - - if len(issueState.Milestones) > 0 && issueState.Milestones[0] == noMilestone { - issueState.Milestones = issueState.Milestones[0:0] + if values.Milestone != "" && values.Milestone != noMilestone { + issueState.Milestones = []string{values.Milestone} } allowPreview = !issueState.HasMetadata() From acf004671818bfdba2f06d3cb6e42083dedf4930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 15:00:29 +0200 Subject: [PATCH 44/59] api command: support `{owner}` and `{repo}` placeholders When `{owner}` and `{repo}` strings are found in request path (for REST requests) or `query` (for GraphQL), they are replaced with values from the repository of the current working directory. --- command/root.go | 7 +++ pkg/cmd/api/api.go | 39 ++++++++++++++- pkg/cmd/api/api_test.go | 104 ++++++++++++++++++++++++++++++++++++++++ pkg/cmdutil/factory.go | 2 + 4 files changed, 151 insertions(+), 1 deletion(-) diff --git a/command/root.go b/command/root.go index 12979ad5f..cfe975779 100644 --- a/command/root.go +++ b/command/root.go @@ -75,8 +75,10 @@ func init() { HttpClient: func() (*http.Client, error) { token := os.Getenv("GITHUB_TOKEN") if len(token) == 0 { + // TODO: decouple from `context` ctx := context.New() var err error + // TODO: pass IOStreams to this so that the auth flow knows if it's interactive or not token, err = ctx.AuthToken() if err != nil { return nil, err @@ -84,6 +86,11 @@ func init() { } return httpClient(token), nil }, + BaseRepo: func() (ghrepo.Interface, error) { + // TODO: decouple from `context` + ctx := context.New() + return ctx.BaseRepo() + }, } RootCmd.AddCommand(apiCmd.NewCmdApi(cmdFactory, nil)) } diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index b4a8dbdff..c7f6dd703 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/jsoncolor" @@ -32,12 +33,14 @@ type ApiOptions struct { ShowResponseHeaders bool HttpClient func() (*http.Client, error) + BaseRepo func() (ghrepo.Interface, error) } func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command { opts := ApiOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, + BaseRepo: f.BaseRepo, } cmd := &cobra.Command{ @@ -93,8 +96,11 @@ func apiRun(opts *ApiOptions) error { return err } + requestPath, params, err := fillPlaceholders(opts, params) + if err != nil { + return fmt.Errorf("unable to expand `{...}` placeholders in query: %w", err) + } method := opts.RequestMethod - requestPath := opts.RequestPath requestHeaders := opts.RequestHeaders var requestBody interface{} = params @@ -170,6 +176,37 @@ func apiRun(opts *ApiOptions) error { return nil } +// fillPlaceholders replaces `{owner}` and `{repo}` placeholders with values from the current repository +func fillPlaceholders(opts *ApiOptions, params map[string]interface{}) (string, map[string]interface{}, error) { + query := opts.RequestPath + isGraphQL := opts.RequestPath == "graphql" + + if isGraphQL { + if q, ok := params["query"].(string); ok { + query = q + } + } + + if !strings.Contains(query, "{owner}") && !strings.Contains(query, "{repo}") { + return opts.RequestPath, params, nil + } + + baseRepo, err := opts.BaseRepo() + if err != nil { + return opts.RequestPath, params, err + } + + query = strings.ReplaceAll(query, "{owner}", baseRepo.RepoOwner()) + query = strings.ReplaceAll(query, "{repo}", baseRepo.RepoName()) + + if isGraphQL { + params["query"] = query + return opts.RequestPath, params, nil + } + + return query, params, nil +} + func printHeaders(w io.Writer, headers http.Header, colorize bool) { var names []string for name := range headers { diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index 100c1257e..0069e4635 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -7,8 +7,10 @@ import ( "io/ioutil" "net/http" "os" + "reflect" "testing" + "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/google/shlex" @@ -451,3 +453,105 @@ func Test_openUserFile(t *testing.T) { assert.Equal(t, int64(13), length) assert.Equal(t, "file contents", string(fb)) } + +func Test_fillPlaceholders(t *testing.T) { + type args struct { + opts *ApiOptions + params map[string]interface{} + } + tests := []struct { + name string + args args + wantPath string + wantParams map[string]interface{} + wantErr bool + }{ + { + name: "no changes", + args: args{ + opts: &ApiOptions{ + RequestPath: "repos/owner/repo/releases", + BaseRepo: nil, + }, + params: map[string]interface{}{ + "query": "{owner}/{repo}", + }, + }, + wantPath: "repos/owner/repo/releases", + wantParams: map[string]interface{}{ + "query": "{owner}/{repo}", + }, + wantErr: false, + }, + { + name: "REST path substitute", + args: args{ + opts: &ApiOptions{ + RequestPath: "repos/{owner}/{repo}/releases", + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + }, + params: map[string]interface{}{ + "query": "{owner}/{repo}", + }, + }, + wantPath: "repos/hubot/robot-uprising/releases", + wantParams: map[string]interface{}{ + "query": "{owner}/{repo}", + }, + wantErr: false, + }, + { + name: "GraphQL query substitute", + args: args{ + opts: &ApiOptions{ + RequestPath: "graphql", + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + }, + params: map[string]interface{}{ + "query": "{owner}/{repo}/pulls/{owner}", + }, + }, + wantPath: "graphql", + wantParams: map[string]interface{}{ + "query": "hubot/robot-uprising/pulls/hubot", + }, + wantErr: false, + }, + { + name: "GraphQL no query", + args: args{ + opts: &ApiOptions{ + RequestPath: "graphql", + BaseRepo: nil, + }, + params: map[string]interface{}{ + "foo": "{owner}/{repo}", + }, + }, + wantPath: "graphql", + wantParams: map[string]interface{}{ + "foo": "{owner}/{repo}", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := fillPlaceholders(tt.args.opts, tt.args.params) + if (err != nil) != tt.wantErr { + t.Errorf("fillPlaceholders() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.wantPath { + t.Errorf("fillPlaceholders() got = %v, want %v", got, tt.wantPath) + } + if !reflect.DeepEqual(got1, tt.wantParams) { + t.Errorf("fillPlaceholders() got1 = %v, want %v", got1, tt.wantParams) + } + }) + } +} diff --git a/pkg/cmdutil/factory.go b/pkg/cmdutil/factory.go index 578b29561..ad7162415 100644 --- a/pkg/cmdutil/factory.go +++ b/pkg/cmdutil/factory.go @@ -3,10 +3,12 @@ package cmdutil import ( "net/http" + "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/iostreams" ) type Factory struct { IOStreams *iostreams.IOStreams HttpClient func() (*http.Client, error) + BaseRepo func() (ghrepo.Interface, error) } From 8e90c27c460d1ca4c296488cd20374f229786d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 16:44:08 +0200 Subject: [PATCH 45/59] Use Cobra's `Example` field for `config get/set` examples --- command/config.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/command/config.go b/command/config.go index a896016a8..a4511061e 100644 --- a/command/config.go +++ b/command/config.go @@ -32,13 +32,10 @@ Current respected settings: var configGetCmd = &cobra.Command{ Use: "get ", - Short: "Prints the value of a given configuration key", - Long: `Get the value for a given configuration key. - -Examples: - - $ gh config get git_protocol - https + Short: "Print the value of a given configuration key", + Example: ` + $ gh config get git_protocol + https `, Args: cobra.ExactArgs(1), RunE: configGet, @@ -46,12 +43,9 @@ Examples: var configSetCmd = &cobra.Command{ Use: "set ", - Short: "Updates configuration with the value of a given key", - Long: `Update the configuration by setting a key to a value. - -Examples: - - $ gh config set editor vim + Short: "Update configuration with a value for the given key", + Example: ` + $ gh config set editor vim `, Args: cobra.ExactArgs(2), RunE: configSet, From e7c934b05c89875de28024a0ab8a2d74c35d0ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 16:44:27 +0200 Subject: [PATCH 46/59] Tweak `gh help config` output --- command/config.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/command/config.go b/command/config.go index a4511061e..e27580bd9 100644 --- a/command/config.go +++ b/command/config.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "github.com/spf13/cobra" ) @@ -21,11 +22,11 @@ func init() { var configCmd = &cobra.Command{ Use: "config", - Short: "Set and get gh settings", - Long: `Get and set key/value strings. + Short: "Manage configuration for gh", + Long: `Display or change configuration settings for gh. Current respected settings: -- git_protocol: https or ssh. Default is https. +- git_protocol: "https" or "ssh". Default is "https". - editor: if unset, defaults to environment variables. `, } From e6a0c3dc283c4c8b6e3b5fcdcf3b2e80d7b51e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 18:47:57 +0200 Subject: [PATCH 47/59] Fix EXAMPLES sections of various commands - Code should be intented per Markdown syntax for a code block. This matters when this documentation is rendered to HTML - Fix some innaccurate usage examples - Tweak wording, formatting in a few places --- command/alias.go | 41 +++++++++++++++++++++++------------------ command/credits.go | 13 +++++++++---- command/gist.go | 29 +++++++++++++++++++---------- command/help.go | 4 +++- command/issue.go | 8 +++++--- command/pr.go | 17 ++++++++++------- command/pr_review.go | 28 +++++++++++++++++----------- command/repo.go | 39 +++++++++++++++++++++++++-------------- command/root.go | 8 +++++--- 9 files changed, 116 insertions(+), 71 deletions(-) diff --git a/command/alias.go b/command/alias.go index da005a4ab..985664768 100644 --- a/command/alias.go +++ b/command/alias.go @@ -23,24 +23,30 @@ var aliasCmd = &cobra.Command{ } var aliasSetCmd = &cobra.Command{ - Use: "set ", - // NB: this allows a user to eschew quotes when specifiying an alias expansion. We'll have to - // revisit it if we ever want to add flags to alias set but we have no current plans for that. - DisableFlagParsing: true, - Short: "Create a shortcut for a gh command", - Long: `This command lets you write your own shortcuts for running gh. They can be simple strings or accept placeholder arguments.`, + Use: "set ", + Short: "Create a shortcut for a gh command", + Long: `Declare a word as a command alias that will expand to the specified command. + +The expansion may specify additional arguments and flags. If the expansion +includes positional placeholders such as '$1', '$2', etc., any extra arguments +that follow the invocation of an alias will be inserted appropriately.`, Example: ` - gh alias set pv 'pr view' - # gh pv -w 123 -> gh pr view -w 123. + $ gh alias set pv 'pr view' + $ gh pv -w 123 + #=> gh pr view -w 123 + + $ gh alias set bugs 'issue list --label="bugs"' - gh alias set bugs 'issue list --label="bugs"' - # gh bugs -> gh issue list --label="bugs". - - gh alias set epicsBy 'issue list --author="$1" --label="epic"' - # gh epicsBy vilmibm -> gh issue list --author="$1" --label="epic" + $ gh alias set epicsBy 'issue list --author="$1" --label="epic"' + $ gh epicsBy vilmibm + #=> gh issue list --author="vilmibm" --label="epic" `, Args: cobra.MinimumNArgs(2), RunE: aliasSet, + + // NB: this allows a user to eschew quotes when specifiying an alias expansion. We'll have to + // revisit it if we ever want to add flags to alias set but we have no current plans for that. + DisableFlagParsing: true, } func aliasSet(cmd *cobra.Command, args []string) error { @@ -168,11 +174,10 @@ func aliasList(cmd *cobra.Command, args []string) error { } var aliasDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Delete an alias.", - Args: cobra.ExactArgs(1), - Example: "gh alias delete co", - RunE: aliasDelete, + Use: "delete ", + Short: "Delete an alias.", + Args: cobra.ExactArgs(1), + RunE: aliasDelete, } func aliasDelete(cmd *cobra.Command, args []string) error { diff --git a/command/credits.go b/command/credits.go index 9d5560bad..f64bbb3df 100644 --- a/command/credits.go +++ b/command/credits.go @@ -44,10 +44,15 @@ var creditsCmd = &cobra.Command{ Use: "credits", Short: "View credits for this tool", Long: `View animated credits for gh, the tool you are currently using :)`, - Example: `gh credits # see a credits animation for this project - gh credits owner/repo # see a credits animation for owner/repo - gh credits -s # display a non-animated thank you - gh credits | cat # just print the contributors, one per line + Example: ` + # see a credits animation for this project + $ gh credits + + # display a non-animated thank you + $ gh credits -s + + # just print the contributors, one per line + $ gh credits | cat `, Args: cobra.ExactArgs(0), RunE: ghCredits, diff --git a/command/gist.go b/command/gist.go index 04e6133fe..765ffc765 100644 --- a/command/gist.go +++ b/command/gist.go @@ -27,20 +27,29 @@ var gistCmd = &cobra.Command{ } var gistCreateCmd = &cobra.Command{ - Use: `create {|-}...`, + Use: `create [... | -]`, Short: "Create a new gist", - Long: `gh gist create: create gists + Long: `Create a new GitHub gist with given contents. -Gists can be created from one or many files. This command can also read from STDIN. By default, gists are private; use --public to change this. +Gists can be created from one or multiple files. Alternatively, pass "-" as +file name to read from standard input. -Examples +By default, gists are private; use '--public' to make publicly listed ones.`, + Example: ` + # publish file 'hello.py' as a public gist + $ gh gist create --public hello.py + + # create a gist with a description + $ gh gist create hello.py -d "my Hello-World program in Python" - gh gist create hello.py # turn file hello.py into a gist - gh gist create --public hello.py # turn file hello.py into a public gist - gh gist create -d"a file!" hello.py # turn file hello.py into a gist, with description - gh gist create hello.py world.py cool.txt # make a gist out of several files - gh gist create - # read from STDIN to create a gist - cat cool.txt | gh gist create # read the output of another command and make a gist out of it + # create a gist containing several files + $ gh gist create hello.py world.py cool.txt + + # read from standard input to create a gist + $ gh gist create - + + # create a gist from output piped from another command + $ cat cool.txt | gh gist create `, RunE: gistCreate, } diff --git a/command/help.go b/command/help.go index ad3561af2..d49b7a4f9 100644 --- a/command/help.go +++ b/command/help.go @@ -101,7 +101,9 @@ Read the manual at http://cli.github.com/manual`}) fmt.Fprintln(out, utils.Bold(e.Title)) for _, l := range strings.Split(strings.Trim(e.Body, "\n\r"), "\n") { - l = strings.Trim(l, " \n\r") + if e.Title == "EXAMPLES" { + l = strings.TrimPrefix(l, "\t") + } fmt.Fprintln(out, " "+l) } } else { diff --git a/command/issue.go b/command/issue.go index d630b340c..e2a4cfe32 100644 --- a/command/issue.go +++ b/command/issue.go @@ -52,9 +52,11 @@ var issueCmd = &cobra.Command{ Use: "issue ", Short: "Create and view issues", Long: `Work with GitHub issues`, - Example: `$ gh issue list -$ gh issue create --fill -$ gh issue view --web`, + Example: ` + $ gh issue list + $ gh issue create --label bug + $ gh issue view --web +`, Annotations: map[string]string{ "IsCore": "true", "help:arguments": `An issue can be supplied as argument in any of the following formats: diff --git a/command/pr.go b/command/pr.go index dce006bf0..89828d866 100644 --- a/command/pr.go +++ b/command/pr.go @@ -49,10 +49,11 @@ var prCmd = &cobra.Command{ Use: "pr ", Short: "Create, view, and checkout pull requests", Long: `Work with GitHub pull requests`, - Example: `$ gh pr checkout 353 -$ gh pr checkout bug-fix-branch -$ gh pr create --fill -$ gh pr view --web`, + Example: ` + $ gh pr checkout 353 + $ gh pr create --fill + $ gh pr view --web +`, Annotations: map[string]string{ "IsCore": "true", "help:arguments": `A pull request can be supplied as argument in any of the following formats: @@ -63,9 +64,11 @@ $ gh pr view --web`, var prListCmd = &cobra.Command{ Use: "list", Short: "List and filter pull requests in this repository", - Example: `$ gh pr list --limit all -$ gh pr list --state closed -$ gh pr list --label “priority 1” “bug”`, + Example: ` + $ gh pr list --limit 999 + $ gh pr list --state closed + $ gh pr list --label "priority 1" --label "bug" +`, RunE: prList, } var prStatusCmd = &cobra.Command{ diff --git a/command/pr_review.go b/command/pr_review.go index 37b4e09c0..f93eaad81 100644 --- a/command/pr_review.go +++ b/command/pr_review.go @@ -23,18 +23,24 @@ func init() { var prReviewCmd = &cobra.Command{ Use: "review [ | | ]", - Short: "Add a review to a pull request.", - Args: cobra.MaximumNArgs(1), - Long: `Add a review to either a specified pull request or the pull request associated with the current branch. + Short: "Add a review to a pull request", + Long: `Add a review to a pull request. -Examples: - - gh pr review # add a review for the current branch's pull request - gh pr review 123 # add a review for pull request 123 - gh pr review -a # mark the current branch's pull request as approved - gh pr review -c -b "interesting" # comment on the current branch's pull request - gh pr review 123 -r -b "needs more ascii art" # request changes on pull request 123 - `, +Without an argument, the pull request that belongs to the current branch is reviewed.`, + Example: ` + # approve the pull request of the current branch + $ gh pr review --approve + + # leave a review comment for the current branch + $ gh pr review --comment -b "interesting" + + # add a review for a specific pull request + $ gh pr review 123 + + # request changes on a specific pull request + $ gh pr review 123 -r -b "needs more ASCII art" +`, + Args: cobra.MaximumNArgs(1), RunE: prReview, } diff --git a/command/repo.go b/command/repo.go index d62f0c715..ab6c1322f 100644 --- a/command/repo.go +++ b/command/repo.go @@ -47,9 +47,10 @@ var repoCmd = &cobra.Command{ Use: "repo ", Short: "Create, clone, fork, and view repositories", Long: `Work with GitHub repositories`, - Example: `$ gh repo create -$ gh repo clone cli/cli -$ gh repo view --web + Example: ` + $ gh repo create + $ gh repo clone cli/cli + $ gh repo view --web `, Annotations: map[string]string{ "IsCore": "true", @@ -75,15 +76,17 @@ To pass 'git clone' flags, separate them with '--'.`, var repoCreateCmd = &cobra.Command{ Use: "create []", Short: "Create a new repository", - Long: `Create a new GitHub repository`, - Example: utils.Bold("$ gh repo create") + ` -Will create a repository on your account using the name of your current directory + Long: `Create a new GitHub repository.`, + Example: ` + # create a repository under your account using the current directory name + $ gh repo create -` + utils.Bold("$ gh repo create my-project") + ` -Will create a repository on your account using the name 'my-project' + # create a repository with a specific name + $ gh repo create my-project -` + utils.Bold("$ gh repo create cli/my-project") + ` -Will create a repository in the organization 'cli' using the name 'my-project'`, + # create a repository in an organization + $ gh repo create cli/my-project +`, Annotations: map[string]string{"help:arguments": `A repository can be supplied as an argument in any of the following formats: - - by URL, e.g. "https://github.com/OWNER/REPO"`}, @@ -113,10 +116,18 @@ With '--web', open the repository in a web browser instead.`, var repoCreditsCmd = &cobra.Command{ Use: "credits []", Short: "View credits for a repository", - Example: `$ gh repo credits # view credits for the current repository -$ gh repo credits cool/repo # view credits for cool/repo -$ gh repo credits -s # print a non-animated thank you -$ gh repo credits | cat # pipe to just print the contributors, one per line + Example: ` + # view credits for the current repository + $ gh repo credits + + # view credits for a specific repository + $ gh repo credits cool/repo + + # print a non-animated thank you + $ gh repo credits -s + + # pipe to just print the contributors, one per line + $ gh repo credits | cat `, Args: cobra.MaximumNArgs(1), RunE: repoCredits, diff --git a/command/root.go b/command/root.go index 12979ad5f..ca30e57a8 100644 --- a/command/root.go +++ b/command/root.go @@ -96,9 +96,11 @@ var RootCmd = &cobra.Command{ SilenceErrors: true, SilenceUsage: true, - Example: `$ gh issue create -$ gh repo clone -$ gh pr checkout 321`, + Example: ` + $ gh issue create + $ gh repo clone cli/cli + $ gh pr checkout 321 +`, Annotations: map[string]string{ "help:feedback": ` Fill out our feedback form https://forms.gle/umxd3h31c7aMQFKG7 From 99fce24fc89d456b653f82444779033d0529a1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 18:50:18 +0200 Subject: [PATCH 48/59] Fix FLAGS section showing up empty for `config get/set` This is because `config get/set` both do have a flag, but it's hidden. --- command/help.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/command/help.go b/command/help.go index d49b7a4f9..7f08071de 100644 --- a/command/help.go +++ b/command/help.go @@ -78,8 +78,9 @@ func rootHelpFunc(command *cobra.Command, args []string) { if len(additionalCommands) > 0 { helpEntries = append(helpEntries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")}) } - if command.HasLocalFlags() { - helpEntries = append(helpEntries, helpEntry{"FLAGS", strings.TrimRight(command.LocalFlags().FlagUsages(), "\n")}) + flagUsages := strings.TrimRight(command.LocalFlags().FlagUsages(), "\n") + if flagUsages != "" { + helpEntries = append(helpEntries, helpEntry{"FLAGS", flagUsages}) } if _, ok := command.Annotations["help:arguments"]; ok { helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]}) From 0dfc0f733fcc0e3b1b8e8a5a91d5b2aa3515fac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 20:31:37 +0200 Subject: [PATCH 49/59] Fix indentation of Example blocks In HTML, `Example` blocks seem to be already injected in fenced Markdown blocks `` ``` ``, so they don't need to be especially intented. --- command/alias.go | 5 +++-- command/credits.go | 5 +++-- command/gist.go | 5 +++-- command/help.go | 7 +++---- command/issue.go | 5 +++-- command/pr.go | 9 +++++---- command/pr_review.go | 5 +++-- command/repo.go | 13 +++++++------ command/root.go | 5 +++-- go.mod | 1 + go.sum | 2 ++ 11 files changed, 36 insertions(+), 26 deletions(-) diff --git a/command/alias.go b/command/alias.go index 985664768..64056f9d5 100644 --- a/command/alias.go +++ b/command/alias.go @@ -5,6 +5,7 @@ import ( "sort" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/utils" "github.com/google/shlex" "github.com/spf13/cobra" @@ -30,7 +31,7 @@ var aliasSetCmd = &cobra.Command{ The expansion may specify additional arguments and flags. If the expansion includes positional placeholders such as '$1', '$2', etc., any extra arguments that follow the invocation of an alias will be inserted appropriately.`, - Example: ` + Example: heredoc.Doc(` $ gh alias set pv 'pr view' $ gh pv -w 123 #=> gh pr view -w 123 @@ -40,7 +41,7 @@ that follow the invocation of an alias will be inserted appropriately.`, $ gh alias set epicsBy 'issue list --author="$1" --label="epic"' $ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic" -`, + `), Args: cobra.MinimumNArgs(2), RunE: aliasSet, diff --git a/command/credits.go b/command/credits.go index f64bbb3df..0104edb02 100644 --- a/command/credits.go +++ b/command/credits.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" @@ -44,7 +45,7 @@ var creditsCmd = &cobra.Command{ Use: "credits", Short: "View credits for this tool", Long: `View animated credits for gh, the tool you are currently using :)`, - Example: ` + Example: heredoc.Doc(` # see a credits animation for this project $ gh credits @@ -53,7 +54,7 @@ var creditsCmd = &cobra.Command{ # just print the contributors, one per line $ gh credits | cat -`, + `), Args: cobra.ExactArgs(0), RunE: ghCredits, Hidden: true, diff --git a/command/gist.go b/command/gist.go index 765ffc765..227f8bd92 100644 --- a/command/gist.go +++ b/command/gist.go @@ -8,6 +8,7 @@ import ( "os" "path" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/utils" "github.com/spf13/cobra" @@ -35,7 +36,7 @@ Gists can be created from one or multiple files. Alternatively, pass "-" as file name to read from standard input. By default, gists are private; use '--public' to make publicly listed ones.`, - Example: ` + Example: heredoc.Doc(` # publish file 'hello.py' as a public gist $ gh gist create --public hello.py @@ -50,7 +51,7 @@ By default, gists are private; use '--public' to make publicly listed ones.`, # create a gist from output piped from another command $ cat cool.txt | gh gist create -`, + `), RunE: gistCreate, } diff --git a/command/help.go b/command/help.go index 7f08071de..d93e779de 100644 --- a/command/help.go +++ b/command/help.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "regexp" "strings" "github.com/cli/cli/utils" @@ -78,7 +79,8 @@ func rootHelpFunc(command *cobra.Command, args []string) { if len(additionalCommands) > 0 { helpEntries = append(helpEntries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")}) } - flagUsages := strings.TrimRight(command.LocalFlags().FlagUsages(), "\n") + dedent := regexp.MustCompile(`(?m)^ `) + flagUsages := dedent.ReplaceAllString(command.LocalFlags().FlagUsages(), "") if flagUsages != "" { helpEntries = append(helpEntries, helpEntry{"FLAGS", flagUsages}) } @@ -102,9 +104,6 @@ Read the manual at http://cli.github.com/manual`}) fmt.Fprintln(out, utils.Bold(e.Title)) for _, l := range strings.Split(strings.Trim(e.Body, "\n\r"), "\n") { - if e.Title == "EXAMPLES" { - l = strings.TrimPrefix(l, "\t") - } fmt.Fprintln(out, " "+l) } } else { diff --git a/command/issue.go b/command/issue.go index e2a4cfe32..8a68e2652 100644 --- a/command/issue.go +++ b/command/issue.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/git" "github.com/cli/cli/internal/ghrepo" @@ -52,11 +53,11 @@ var issueCmd = &cobra.Command{ Use: "issue ", Short: "Create and view issues", Long: `Work with GitHub issues`, - Example: ` + Example: heredoc.Doc(` $ gh issue list $ gh issue create --label bug $ gh issue view --web -`, + `), Annotations: map[string]string{ "IsCore": "true", "help:arguments": `An issue can be supplied as argument in any of the following formats: diff --git a/command/pr.go b/command/pr.go index 89828d866..adf8347ab 100644 --- a/command/pr.go +++ b/command/pr.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/context" "github.com/cli/cli/git" @@ -49,11 +50,11 @@ var prCmd = &cobra.Command{ Use: "pr ", Short: "Create, view, and checkout pull requests", Long: `Work with GitHub pull requests`, - Example: ` + Example: heredoc.Doc(` $ gh pr checkout 353 $ gh pr create --fill $ gh pr view --web -`, + `), Annotations: map[string]string{ "IsCore": "true", "help:arguments": `A pull request can be supplied as argument in any of the following formats: @@ -64,11 +65,11 @@ var prCmd = &cobra.Command{ var prListCmd = &cobra.Command{ Use: "list", Short: "List and filter pull requests in this repository", - Example: ` + Example: heredoc.Doc(` $ gh pr list --limit 999 $ gh pr list --state closed $ gh pr list --label "priority 1" --label "bug" -`, + `), RunE: prList, } var prStatusCmd = &cobra.Command{ diff --git a/command/pr_review.go b/command/pr_review.go index f93eaad81..08efc638d 100644 --- a/command/pr_review.go +++ b/command/pr_review.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2" + "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" "github.com/cli/cli/api" @@ -27,7 +28,7 @@ var prReviewCmd = &cobra.Command{ Long: `Add a review to a pull request. Without an argument, the pull request that belongs to the current branch is reviewed.`, - Example: ` + Example: heredoc.Doc(` # approve the pull request of the current branch $ gh pr review --approve @@ -39,7 +40,7 @@ Without an argument, the pull request that belongs to the current branch is revi # request changes on a specific pull request $ gh pr review 123 -r -b "needs more ASCII art" -`, + `), Args: cobra.MaximumNArgs(1), RunE: prReview, } diff --git a/command/repo.go b/command/repo.go index ab6c1322f..7c15945aa 100644 --- a/command/repo.go +++ b/command/repo.go @@ -10,6 +10,7 @@ import ( "time" "github.com/AlecAivazis/survey/v2" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/git" "github.com/cli/cli/internal/ghrepo" @@ -47,11 +48,11 @@ var repoCmd = &cobra.Command{ Use: "repo ", Short: "Create, clone, fork, and view repositories", Long: `Work with GitHub repositories`, - Example: ` + Example: heredoc.Doc(` $ gh repo create $ gh repo clone cli/cli $ gh repo view --web -`, + `), Annotations: map[string]string{ "IsCore": "true", "help:arguments": ` @@ -77,7 +78,7 @@ var repoCreateCmd = &cobra.Command{ Use: "create []", Short: "Create a new repository", Long: `Create a new GitHub repository.`, - Example: ` + Example: heredoc.Doc(` # create a repository under your account using the current directory name $ gh repo create @@ -86,7 +87,7 @@ var repoCreateCmd = &cobra.Command{ # create a repository in an organization $ gh repo create cli/my-project -`, + `), Annotations: map[string]string{"help:arguments": `A repository can be supplied as an argument in any of the following formats: - - by URL, e.g. "https://github.com/OWNER/REPO"`}, @@ -116,7 +117,7 @@ With '--web', open the repository in a web browser instead.`, var repoCreditsCmd = &cobra.Command{ Use: "credits []", Short: "View credits for a repository", - Example: ` + Example: heredoc.Doc(` # view credits for the current repository $ gh repo credits @@ -128,7 +129,7 @@ var repoCreditsCmd = &cobra.Command{ # pipe to just print the contributors, one per line $ gh repo credits | cat -`, + `), Args: cobra.MaximumNArgs(1), RunE: repoCredits, Hidden: true, diff --git a/command/root.go b/command/root.go index ca30e57a8..92a25e67f 100644 --- a/command/root.go +++ b/command/root.go @@ -10,6 +10,7 @@ import ( "runtime/debug" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/context" "github.com/cli/cli/internal/config" @@ -96,11 +97,11 @@ var RootCmd = &cobra.Command{ SilenceErrors: true, SilenceUsage: true, - Example: ` + Example: heredoc.Doc(` $ gh issue create $ gh repo clone cli/cli $ gh pr checkout 321 -`, + `), Annotations: map[string]string{ "help:feedback": ` Fill out our feedback form https://forms.gle/umxd3h31c7aMQFKG7 diff --git a/go.mod b/go.mod index 7f1484a03..6857e1947 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/AlecAivazis/survey/v2 v2.0.7 + github.com/MakeNowJust/heredoc v1.0.0 github.com/briandowns/spinner v1.11.1 github.com/charmbracelet/glamour v0.1.1-0.20200320173916-301d3bcf3058 github.com/dlclark/regexp2 v1.2.0 // indirect diff --git a/go.sum b/go.sum index d0d7585bf..42f30698a 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= From d5c4e23ca5e4f6c6fd2af569a0a66584d9de1704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 20:36:08 +0200 Subject: [PATCH 50/59] Tweak last few Example sections --- command/config.go | 9 +++++---- command/help.go | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/command/config.go b/command/config.go index e27580bd9..7f34b8687 100644 --- a/command/config.go +++ b/command/config.go @@ -3,6 +3,7 @@ package command import ( "fmt" + "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" ) @@ -34,10 +35,10 @@ Current respected settings: var configGetCmd = &cobra.Command{ Use: "get ", Short: "Print the value of a given configuration key", - Example: ` + Example: heredoc.Doc(` $ gh config get git_protocol https -`, + `), Args: cobra.ExactArgs(1), RunE: configGet, } @@ -45,9 +46,9 @@ var configGetCmd = &cobra.Command{ var configSetCmd = &cobra.Command{ Use: "set ", Short: "Update configuration with a value for the given key", - Example: ` + Example: heredoc.Doc(` $ gh config set editor vim -`, + `), Args: cobra.ExactArgs(2), RunE: configSet, } diff --git a/command/help.go b/command/help.go index d93e779de..e243577a4 100644 --- a/command/help.go +++ b/command/help.go @@ -79,10 +79,11 @@ func rootHelpFunc(command *cobra.Command, args []string) { if len(additionalCommands) > 0 { helpEntries = append(helpEntries, helpEntry{"ADDITIONAL COMMANDS", strings.Join(additionalCommands, "\n")}) } - dedent := regexp.MustCompile(`(?m)^ `) - flagUsages := dedent.ReplaceAllString(command.LocalFlags().FlagUsages(), "") + + flagUsages := command.LocalFlags().FlagUsages() if flagUsages != "" { - helpEntries = append(helpEntries, helpEntry{"FLAGS", flagUsages}) + dedent := regexp.MustCompile(`(?m)^ `) + helpEntries = append(helpEntries, helpEntry{"FLAGS", dedent.ReplaceAllString(flagUsages, "")}) } if _, ok := command.Annotations["help:arguments"]; ok { helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]}) From 47102fe4277ebd7346317573c2b9eae98bef60ab Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 11 Jun 2020 14:04:29 -0500 Subject: [PATCH 51/59] just fill in the blank config with default content --- context/context.go | 5 +---- internal/config/config_type.go | 19 ++----------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/context/context.go b/context/context.go index 24f81142c..236f9e722 100644 --- a/context/context.go +++ b/context/context.go @@ -165,10 +165,7 @@ func (c *fsContext) Config() (config.Config, error) { if c.config == nil { cfg, err := config.ParseDefaultConfig() if errors.Is(err, os.ErrNotExist) { - cfg, err = config.InitDefaultConfig() - if err != nil { - return nil, fmt.Errorf("could not create default config: %w", err) - } + cfg = config.NewBlankConfig() } else if err != nil { return nil, err } diff --git a/internal/config/config_type.go b/internal/config/config_type.go index 7f98341b3..a57d21dec 100644 --- a/internal/config/config_type.go +++ b/internal/config/config_type.go @@ -122,8 +122,8 @@ func NewConfig(root *yaml.Node) Config { } } -func InitDefaultConfig() (Config, error) { - cfg := NewConfig(&yaml.Node{ +func NewBlankConfig() Config { + return NewConfig(&yaml.Node{ Kind: yaml.DocumentNode, Content: []*yaml.Node{ { @@ -169,21 +169,6 @@ func InitDefaultConfig() (Config, error) { }, }, }) - - err := cfg.Write() - if err != nil { - return nil, err - } - - return cfg, nil - -} - -func NewBlankConfig() Config { - return NewConfig(&yaml.Node{ - Kind: yaml.DocumentNode, - Content: []*yaml.Node{{Kind: yaml.MappingNode}}, - }) } // This type implements a Config interface and represents a config file on disk. From 64a7fd420091a9da94f78003474781e0d4a44050 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 11 Jun 2020 14:04:41 -0500 Subject: [PATCH 52/59] test default configuration --- internal/config/config_file_test.go | 9 +++++---- internal/config/config_type_test.go | 26 +++++++++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/internal/config/config_file_test.go b/internal/config/config_file_test.go index edc3a6ae9..d19130bb5 100644 --- a/internal/config/config_file_test.go +++ b/internal/config/config_file_test.go @@ -7,6 +7,7 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -96,16 +97,16 @@ github.com: defer StubBackupConfig()() _, err := ParseConfig("config.yml") - eq(t, err, nil) + assert.Nil(t, err) - expectedMain := "" + expectedMain := "# What protocol to use when performing git operations. Supported values: ssh, https\ngit_protocol: https\n# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.\neditor:\n# Aliases allow you to create nicknames for gh commands\naliases:\n co: pr checkout\n" expectedHosts := `github.com: user: keiyuri oauth_token: "123456" ` - eq(t, mainBuf.String(), expectedMain) - eq(t, hostsBuf.String(), expectedHosts) + assert.Equal(t, expectedMain, mainBuf.String()) + assert.Equal(t, expectedHosts, hostsBuf.String()) } func Test_parseConfigFile(t *testing.T) { diff --git a/internal/config/config_type_test.go b/internal/config/config_type_test.go index 1c8105186..df33effa3 100644 --- a/internal/config/config_type_test.go +++ b/internal/config/config_type_test.go @@ -19,7 +19,8 @@ func Test_fileConfig_Set(t *testing.T) { assert.NoError(t, c.Set("github.com", "user", "hubot")) assert.NoError(t, c.Write()) - assert.Equal(t, "editor: nano\n", mainBuf.String()) + expected := "# What protocol to use when performing git operations. Supported values: ssh, https\ngit_protocol: https\n# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.\neditor: nano\n# Aliases allow you to create nicknames for gh commands\naliases:\n co: pr checkout\n" + assert.Equal(t, expected, mainBuf.String()) assert.Equal(t, `github.com: git_protocol: ssh user: hubot @@ -28,14 +29,29 @@ example.com: `, hostsBuf.String()) } -func Test_fileConfig_Write(t *testing.T) { +func Test_defaultConfig(t *testing.T) { mainBuf := bytes.Buffer{} hostsBuf := bytes.Buffer{} defer StubWriteConfig(&mainBuf, &hostsBuf)() - c := NewBlankConfig() - assert.NoError(t, c.Write()) + cfg := NewBlankConfig() + assert.NoError(t, cfg.Write()) - assert.Equal(t, "", mainBuf.String()) + expected := "# What protocol to use when performing git operations. Supported values: ssh, https\ngit_protocol: https\n# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.\neditor:\n# Aliases allow you to create nicknames for gh commands\naliases:\n co: pr checkout\n" + assert.Equal(t, expected, mainBuf.String()) assert.Equal(t, "", hostsBuf.String()) + + proto, err := cfg.Get("", "git_protocol") + assert.Nil(t, err) + assert.Equal(t, "https", proto) + + editor, err := cfg.Get("", "editor") + assert.Nil(t, err) + assert.Equal(t, "", editor) + + aliases, err := cfg.Aliases() + assert.Nil(t, err) + assert.Equal(t, len(aliases.All()), 1) + expansion, _ := aliases.Get("co") + assert.Equal(t, expansion, "pr checkout") } From 3f6d0bff45d8d52be9fd3153f823a796c246ce2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 11 Jun 2020 21:46:27 +0200 Subject: [PATCH 53/59] Switch to `:owner`/`:repo` syntax for placeholders --- go.mod | 1 + go.sum | 2 + pkg/cmd/api/api.go | 73 +++++++++++++++---------- pkg/cmd/api/api_test.go | 117 +++++++++++++++++----------------------- 4 files changed, 95 insertions(+), 98 deletions(-) diff --git a/go.mod b/go.mod index 7f1484a03..6857e1947 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/AlecAivazis/survey/v2 v2.0.7 + github.com/MakeNowJust/heredoc v1.0.0 github.com/briandowns/spinner v1.11.1 github.com/charmbracelet/glamour v0.1.1-0.20200320173916-301d3bcf3058 github.com/dlclark/regexp2 v1.2.0 // indirect diff --git a/go.sum b/go.sum index d0d7585bf..42f30698a 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index c7f6dd703..a5540cf2d 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" @@ -48,13 +49,16 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command Short: "Make an authenticated GitHub API request", Long: `Makes an authenticated HTTP request to the GitHub API and prints the response. -The argument should either be a path of a GitHub API v3 endpoint, or +The endpoint argument should either be a path of a GitHub API v3 endpoint, or "graphql" to access the GitHub API v4. +Placeholder values ":owner" and ":repo" in the endpoint argument will get replaced +with values from the repository of the current directory. + The default HTTP request method is "GET" normally and "POST" if any parameters were added. Override the method with '--method'. -Pass one or more '--raw-field' values in "=" format to add +Pass one or more '--raw-field' values in "key=value" format to add JSON-encoded string parameters to the POST body. The '--field' flag behaves like '--raw-field' with magic type conversion based @@ -62,6 +66,8 @@ on the format of the value: - literal values "true", "false", "null", and integer numbers get converted to appropriate JSON types; +- placeholder values ":owner" and ":repo" get populated with values from the + repository of the current directory; - if the value starts with "@", the rest of the value is interpreted as a filename to read the value from. Pass "-" to read from standard input. @@ -69,6 +75,19 @@ Raw request body may be passed from the outside via a file specified by '--input Pass "-" to read from standard input. In this mode, parameters specified via '--field' flags are serialized into URL query parameters. `, + Example: heredoc.Doc(` + $ gh api repos/:owner/:repo/releases + + $ gh api graphql -F owner=':owner' -F name=':repo' -f query=' + query($name: String!, $owner: String!) { + repository(owner: $owner, name: $name) { + releases(last: 3) { + nodes { tagName } + } + } + } + ' + `), Args: cobra.ExactArgs(1), RunE: func(c *cobra.Command, args []string) error { opts.RequestPath = args[0] @@ -96,9 +115,9 @@ func apiRun(opts *ApiOptions) error { return err } - requestPath, params, err := fillPlaceholders(opts, params) + requestPath, err := fillPlaceholders(opts.RequestPath, opts) if err != nil { - return fmt.Errorf("unable to expand `{...}` placeholders in query: %w", err) + return fmt.Errorf("unable to expand placeholder in path: %w", err) } method := opts.RequestMethod requestHeaders := opts.RequestHeaders @@ -176,35 +195,31 @@ func apiRun(opts *ApiOptions) error { return nil } -// fillPlaceholders replaces `{owner}` and `{repo}` placeholders with values from the current repository -func fillPlaceholders(opts *ApiOptions, params map[string]interface{}) (string, map[string]interface{}, error) { - query := opts.RequestPath - isGraphQL := opts.RequestPath == "graphql" +var placeholderRE = regexp.MustCompile(`\:(owner|repo)\b`) - if isGraphQL { - if q, ok := params["query"].(string); ok { - query = q - } - } - - if !strings.Contains(query, "{owner}") && !strings.Contains(query, "{repo}") { - return opts.RequestPath, params, nil +// fillPlaceholders populates `:owner` and `:repo` placeholders with values from the current repository +func fillPlaceholders(value string, opts *ApiOptions) (string, error) { + if !placeholderRE.MatchString(value) { + return value, nil } baseRepo, err := opts.BaseRepo() if err != nil { - return opts.RequestPath, params, err + return value, err } - query = strings.ReplaceAll(query, "{owner}", baseRepo.RepoOwner()) - query = strings.ReplaceAll(query, "{repo}", baseRepo.RepoName()) + value = placeholderRE.ReplaceAllStringFunc(value, func(m string) string { + switch m { + case ":owner": + return baseRepo.RepoOwner() + case ":repo": + return baseRepo.RepoName() + default: + panic(fmt.Sprintf("invalid placeholder: %q", m)) + } + }) - if isGraphQL { - params["query"] = query - return opts.RequestPath, params, nil - } - - return query, params, nil + return value, nil } func printHeaders(w io.Writer, headers http.Header, colorize bool) { @@ -241,7 +256,7 @@ func parseFields(opts *ApiOptions) (map[string]interface{}, error) { if err != nil { return params, err } - value, err := magicFieldValue(strValue, opts.IO.In) + value, err := magicFieldValue(strValue, opts) if err != nil { return params, fmt.Errorf("error parsing %q value: %w", key, err) } @@ -258,9 +273,9 @@ func parseField(f string) (string, string, error) { return f[0:idx], f[idx+1:], nil } -func magicFieldValue(v string, stdin io.ReadCloser) (interface{}, error) { +func magicFieldValue(v string, opts *ApiOptions) (interface{}, error) { if strings.HasPrefix(v, "@") { - return readUserFile(v[1:], stdin) + return readUserFile(v[1:], opts.IO.In) } if n, err := strconv.Atoi(v); err == nil { @@ -275,7 +290,7 @@ func magicFieldValue(v string, stdin io.ReadCloser) (interface{}, error) { case "null": return nil, nil default: - return v, nil + return fillPlaceholders(v, opts) } } diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index 0069e4635..605e4bf9e 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -3,11 +3,9 @@ package api import ( "bytes" "fmt" - "io" "io/ioutil" "net/http" "os" - "reflect" "testing" "github.com/cli/cli/internal/ghrepo" @@ -368,9 +366,11 @@ func Test_magicFieldValue(t *testing.T) { f.Close() t.Cleanup(func() { os.Remove(f.Name()) }) + io, _, _, _ := iostreams.Test() + type args struct { - v string - stdin io.ReadCloser + v string + opts *ApiOptions } tests := []struct { name string @@ -403,21 +403,41 @@ func Test_magicFieldValue(t *testing.T) { wantErr: false, }, { - name: "file", - args: args{v: "@" + f.Name()}, + name: "placeholder", + args: args{ + v: ":owner", + opts: &ApiOptions{ + IO: io, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + }, + }, + want: "hubot", + wantErr: false, + }, + { + name: "file", + args: args{ + v: "@" + f.Name(), + opts: &ApiOptions{IO: io}, + }, want: []byte("file contents"), wantErr: false, }, { - name: "file error", - args: args{v: "@"}, + name: "file error", + args: args{ + v: "@", + opts: &ApiOptions{IO: io}, + }, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := magicFieldValue(tt.args.v, tt.args.stdin) + got, err := magicFieldValue(tt.args.v, tt.args.opts) if (err != nil) != tt.wantErr { t.Errorf("magicFieldValue() error = %v, wantErr %v", err, tt.wantErr) return @@ -456,101 +476,60 @@ func Test_openUserFile(t *testing.T) { func Test_fillPlaceholders(t *testing.T) { type args struct { - opts *ApiOptions - params map[string]interface{} + value string + opts *ApiOptions } tests := []struct { - name string - args args - wantPath string - wantParams map[string]interface{} - wantErr bool + name string + args args + want string + wantErr bool }{ { name: "no changes", args: args{ + value: "repos/owner/repo/releases", opts: &ApiOptions{ - RequestPath: "repos/owner/repo/releases", - BaseRepo: nil, - }, - params: map[string]interface{}{ - "query": "{owner}/{repo}", + BaseRepo: nil, }, }, - wantPath: "repos/owner/repo/releases", - wantParams: map[string]interface{}{ - "query": "{owner}/{repo}", - }, + want: "repos/owner/repo/releases", wantErr: false, }, { - name: "REST path substitute", + name: "has substitutes", args: args{ + value: "repos/:owner/:repo/releases", opts: &ApiOptions{ - RequestPath: "repos/{owner}/{repo}/releases", BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("hubot", "robot-uprising"), nil }, }, - params: map[string]interface{}{ - "query": "{owner}/{repo}", - }, - }, - wantPath: "repos/hubot/robot-uprising/releases", - wantParams: map[string]interface{}{ - "query": "{owner}/{repo}", }, + want: "repos/hubot/robot-uprising/releases", wantErr: false, }, { - name: "GraphQL query substitute", + name: "no greedy substitutes", args: args{ + value: ":ownership/:repository", opts: &ApiOptions{ - RequestPath: "graphql", - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("hubot", "robot-uprising"), nil - }, - }, - params: map[string]interface{}{ - "query": "{owner}/{repo}/pulls/{owner}", + BaseRepo: nil, }, }, - wantPath: "graphql", - wantParams: map[string]interface{}{ - "query": "hubot/robot-uprising/pulls/hubot", - }, - wantErr: false, - }, - { - name: "GraphQL no query", - args: args{ - opts: &ApiOptions{ - RequestPath: "graphql", - BaseRepo: nil, - }, - params: map[string]interface{}{ - "foo": "{owner}/{repo}", - }, - }, - wantPath: "graphql", - wantParams: map[string]interface{}{ - "foo": "{owner}/{repo}", - }, + want: ":ownership/:repository", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1, err := fillPlaceholders(tt.args.opts, tt.args.params) + got, err := fillPlaceholders(tt.args.value, tt.args.opts) if (err != nil) != tt.wantErr { t.Errorf("fillPlaceholders() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.wantPath { - t.Errorf("fillPlaceholders() got = %v, want %v", got, tt.wantPath) - } - if !reflect.DeepEqual(got1, tt.wantParams) { - t.Errorf("fillPlaceholders() got1 = %v, want %v", got1, tt.wantParams) + if got != tt.want { + t.Errorf("fillPlaceholders() got = %v, want %v", got, tt.want) } }) } From 8ffd0d59f618663d6a98ef2f7563a29a1a866cc0 Mon Sep 17 00:00:00 2001 From: gedenata Date: Fri, 12 Jun 2020 04:23:51 +0800 Subject: [PATCH 54/59] redundant type composite literal --- context/remote_test.go | 8 ++++---- git/git_test.go | 6 +++--- git/ssh_config_test.go | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/context/remote_test.go b/context/remote_test.go index 9cf7a0adf..1e0f47ba0 100644 --- a/context/remote_test.go +++ b/context/remote_test.go @@ -95,12 +95,12 @@ func Test_resolvedRemotes_triangularSetup(t *testing.T) { }, Network: api.RepoNetworkResult{ Repositories: []*api.Repository{ - &api.Repository{ + { Name: "NEWNAME", Owner: api.RepositoryOwner{Login: "NEWOWNER"}, ViewerPermission: "READ", }, - &api.Repository{ + { Name: "REPO", Owner: api.RepositoryOwner{Login: "MYSELF"}, ViewerPermission: "ADMIN", @@ -163,7 +163,7 @@ func Test_resolvedRemotes_forkLookup(t *testing.T) { }, Network: api.RepoNetworkResult{ Repositories: []*api.Repository{ - &api.Repository{ + { Name: "NEWNAME", Owner: api.RepositoryOwner{Login: "NEWOWNER"}, ViewerPermission: "READ", @@ -196,7 +196,7 @@ func Test_resolvedRemotes_clonedFork(t *testing.T) { }, Network: api.RepoNetworkResult{ Repositories: []*api.Repository{ - &api.Repository{ + { Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}, ViewerPermission: "ADMIN", diff --git a/git/git_test.go b/git/git_test.go index c84023e40..8cd97a10d 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -15,9 +15,9 @@ func Test_UncommittedChangeCount(t *testing.T) { Output string } cases := []c{ - c{Label: "no changes", Expected: 0, Output: ""}, - c{Label: "one change", Expected: 1, Output: " M poem.txt"}, - c{Label: "untracked file", Expected: 2, Output: " M poem.txt\n?? new.txt"}, + {Label: "no changes", Expected: 0, Output: ""}, + {Label: "one change", Expected: 1, Output: " M poem.txt"}, + {Label: "untracked file", Expected: 2, Output: " M poem.txt\n?? new.txt"}, } teardown := run.SetPrepareCmd(func(*exec.Cmd) run.Runnable { diff --git a/git/ssh_config_test.go b/git/ssh_config_test.go index 35a0c93e6..28f339aa6 100644 --- a/git/ssh_config_test.go +++ b/git/ssh_config_test.go @@ -36,9 +36,9 @@ func Test_Translator(t *testing.T) { tr := m.Translator() cases := [][]string{ - []string{"ssh://gh/o/r", "ssh://github.com/o/r"}, - []string{"ssh://github.com/o/r", "ssh://github.com/o/r"}, - []string{"https://gh/o/r", "https://gh/o/r"}, + {"ssh://gh/o/r", "ssh://github.com/o/r"}, + {"ssh://github.com/o/r", "ssh://github.com/o/r"}, + {"https://gh/o/r", "https://gh/o/r"}, } for _, c := range cases { u, _ := url.Parse(c[0]) From ba9370ab1fcd6d104cb1b566ec8fab7de319e0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 12 Jun 2020 15:59:25 +0200 Subject: [PATCH 55/59] Enable `simplifycompositelit` check in CI This will fail the build when there are redundant types in composite struct literals. --- .golangci.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..57e53d6fb --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,3 @@ +linters: + enable: + gofmt From ab2540652d8c00496329f06e41451023b18cc660 Mon Sep 17 00:00:00 2001 From: Adam Stiles Date: Fri, 12 Jun 2020 07:55:37 -0700 Subject: [PATCH 56/59] Direct users to https url for manual --- command/help.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/help.go b/command/help.go index e243577a4..b776d23f1 100644 --- a/command/help.go +++ b/command/help.go @@ -93,7 +93,7 @@ func rootHelpFunc(command *cobra.Command, args []string) { } helpEntries = append(helpEntries, helpEntry{"LEARN MORE", ` Use "gh --help" for more information about a command. -Read the manual at http://cli.github.com/manual`}) +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"]}) } From 5851ee26ddb97ca8839217e869dc97f28e5491d9 Mon Sep 17 00:00:00 2001 From: ObliviousParadigm Date: Mon, 15 Jun 2020 01:03:27 +0530 Subject: [PATCH 57/59] DOC: Changed few sentences Moved code blocks to new lines to maintain consistency --- README.md | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index aae399b64..227090279 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ the terminal next to where you are already working with `git` and your code. ## Availability -While in beta, GitHub CLI is available for repos hosted on GitHub.com only. It does not currently support repositories hosted on GitHub Enterprise Server or other hosting providers. We are planning support for GitHub Enterprise Server after GitHub CLI is out of beta (likely toward the end of 2020), and we want to ensure that the API endpoints we use are more widely available for GHES versions that most GitHub customers are on. +While in beta, GitHub CLI is available for repos hosted on GitHub.com only. It currently does not support repositories hosted on GitHub Enterprise Server or other hosting providers. We are planning on adding support for GitHub Enterprise Server after GitHub CLI is out of beta (likely towards the end of 2020), and we want to ensure that the API endpoints we use are more widely available for GHES versions that most GitHub customers are on. ## We need your feedback -GitHub CLI is currently early in its development, and we're hoping to get feedback from people using it. +GitHub CLI is currently in its early development stages, and we're hoping to get feedback from people using it. If you've installed and used `gh`, we'd love for you to take a short survey here (no more than five minutes): https://forms.gle/umxd3h31c7aMQFKG7 @@ -31,9 +31,9 @@ Read the [official docs](https://cli.github.com/manual/) for more information. ## Comparison with hub -For many years, [hub][] was the unofficial GitHub CLI tool. `gh` is a new project for us to explore +For many years, [hub][] was the unofficial GitHub CLI tool. `gh` is a new project that helps us explore what an official GitHub CLI tool can look like with a fundamentally different design. While both -tools bring GitHub to the terminal, `hub` behaves as a proxy to `git` and `gh` is a standalone +tools bring GitHub to the terminal, `hub` behaves as a proxy to `git`, and `gh` is a standalone tool. Check out our [more detailed explanation](/docs/gh-vs-hub.md) to learn more. @@ -46,15 +46,31 @@ tool. Check out our [more detailed explanation](/docs/gh-vs-hub.md) to learn mor #### Homebrew -Install: `brew install github/gh/gh` +Install: -Upgrade: `brew upgrade gh` +```bash +brew install github/gh/gh +``` + +Upgrade: + +```bash +brew upgrade gh +``` #### MacPorts -Install: `sudo port install gh` +Install: -Upgrade: `sudo port selfupdate && sudo port upgrade gh` +```bash +sudo port install gh +``` + +Upgrade: + +```bash +sudo port selfupdate && sudo port upgrade gh +``` ### Windows @@ -64,24 +80,28 @@ Upgrade: `sudo port selfupdate && sudo port upgrade gh` Install: -``` +```powershell scoop bucket add github-gh https://github.com/cli/scoop-gh.git scoop install gh ``` -Upgrade: `scoop update gh` +Upgrade: + +```powershell +scoop update gh +``` #### Chocolatey Install: -``` +```powershell choco install gh ``` Upgrade: -``` +```powershell choco upgrade gh ``` @@ -122,7 +142,7 @@ Install and upgrade: Arch Linux users can install from the AUR: https://aur.archlinux.org/packages/github-cli/ ```bash -$ yay -S github-cli +yay -S github-cli ``` ### Other platforms From 21a96baf933498bb48a90e8e6cdebd5dea53ea99 Mon Sep 17 00:00:00 2001 From: ShubhankarKG Date: Mon, 15 Jun 2020 17:45:04 +0530 Subject: [PATCH 58/59] Added description to nfpms --- .goreleaser.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index ceb22c12e..0909fa8f7 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -82,6 +82,7 @@ nfpms: bindir: /usr/local dependencies: - git + description: GitHub’s official command line tool. formats: - deb - rpm From c70756d596a08feb7a469b6876d4ad1e416cdabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez=20Gonzales?= Date: Mon, 15 Jun 2020 08:27:30 -0500 Subject: [PATCH 59/59] Improve message when draft pr is created (#1202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1199 Co-authored-by: Mislav Marohnić --- command/pr_create.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/command/pr_create.go b/command/pr_create.go index 608656f4b..5eafdbcbe 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -192,8 +192,18 @@ func prCreate(cmd *cobra.Command, _ []string) error { } } + isDraft, err := cmd.Flags().GetBool("draft") + if err != nil { + return fmt.Errorf("could not parse draft: %w", err) + } + if !isWeb && !autofill { - fmt.Fprintf(colorableErr(cmd), "\nCreating pull request for %s into %s in %s\n\n", + message := "\nCreating pull request for %s into %s in %s\n\n" + if isDraft { + message = "\nCreating draft pull request for %s into %s in %s\n\n" + } + + fmt.Fprintf(colorableErr(cmd), message, utils.Cyan(headBranch), utils.Cyan(baseBranch), ghrepo.FullName(baseRepo)) @@ -245,10 +255,6 @@ func prCreate(cmd *cobra.Command, _ []string) error { return errors.New("pull request title must not be blank") } - isDraft, err := cmd.Flags().GetBool("draft") - if err != nil { - return fmt.Errorf("could not parse draft: %w", err) - } if isDraft && isWeb { return errors.New("the --draft flag is not supported with --web") }