From 58f62a4a155cb8ea3e6ac686e0fdd6764b5613b9 Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 13:00:34 -0400 Subject: [PATCH 01/18] [Feature] Create repository from template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mislav Marohnić --- api/queries_user.go | 11 ++++++ pkg/cmd/repo/create/create.go | 38 ++++++++++++++++++++ pkg/cmd/repo/create/http.go | 67 +++++++++++++++++++++++++++++++---- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/api/queries_user.go b/api/queries_user.go index 0a9b68bd2..4ce7a3c46 100644 --- a/api/queries_user.go +++ b/api/queries_user.go @@ -14,3 +14,14 @@ func CurrentLoginName(client *Client, hostname string) (string, error) { err := gql.QueryNamed(context.Background(), "UserCurrent", &query, nil) return query.Viewer.Login, err } + +func CurrentUserID(client *Client, hostname string) (string, error) { + var query struct { + Viewer struct { + ID string + } + } + gql := graphQLClient(client.http, hostname) + err := gql.QueryNamed(context.Background(), "UserCurrent", &query, nil) + return query.Viewer.ID, err +} diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 7628184e5..28d8384cc 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -8,8 +8,10 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/api" "github.com/cli/cli/git" "github.com/cli/cli/internal/config" + "github.com/cli/cli/internal/ghinstance" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/internal/run" "github.com/cli/cli/pkg/cmdutil" @@ -28,6 +30,7 @@ type CreateOptions struct { Description string Homepage string Team string + Template string EnableIssues bool EnableWiki bool Public bool @@ -80,6 +83,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of repository") cmd.Flags().StringVarP(&opts.Homepage, "homepage", "h", "", "Repository home page URL") cmd.Flags().StringVarP(&opts.Team, "team", "t", "", "The name of the organization team to be granted access") + cmd.Flags().StringVarP(&opts.Template, "template", "p", "", "Make the new repository based on a template repository") cmd.Flags().BoolVar(&opts.EnableIssues, "enable-issues", true, "Enable issues in the new repository") cmd.Flags().BoolVar(&opts.EnableWiki, "enable-wiki", true, "Enable wiki in the new repository") cmd.Flags().BoolVar(&opts.Public, "public", false, "Make the new repository public") @@ -164,11 +168,45 @@ func createRun(opts *CreateOptions) error { } } + // find template ID + + if opts.Template != "" { + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + var toView ghrepo.Interface + apiClient := api.NewClientFromHTTP(httpClient) + + // var err errors + viewURL := opts.Template + 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) + } + + repo, err := api.GitHubRepo(apiClient, toView) + if err != nil { + return err + } + + opts.Template = repo.ID + } + input := repoCreateInput{ Name: repoToCreate.RepoName(), Visibility: visibility, OwnerID: repoToCreate.RepoOwner(), TeamID: opts.Team, + RepositoryID: opts.Template, Description: opts.Description, HomepageURL: opts.Homepage, HasIssuesEnabled: opts.EnableIssues, diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index 3a46c0d9a..f3623c6e2 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -14,6 +14,8 @@ type repoCreateInput struct { HomepageURL string `json:"homepageUrl,omitempty"` Description string `json:"description,omitempty"` + RepositoryID string `json:"repositoryId,omitempty"` + OwnerID string `json:"ownerId,omitempty"` TeamID string `json:"teamId,omitempty"` @@ -21,16 +23,18 @@ type repoCreateInput struct { HasWikiEnabled bool `json:"hasWikiEnabled"` } +type repoTemplateInput struct { + Name string `json:"name"` + Visibility string `json:"visibility"` + OwnerID string `json:"ownerId,omitempty"` + + RepositoryID string `json:"repositoryId,omitempty"` +} + // repoCreate creates a new GitHub repository func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*api.Repository, error) { apiClient := api.NewClientFromHTTP(client) - var response struct { - CreateRepository struct { - Repository api.Repository - } - } - if input.TeamID != "" { orgID, teamID, err := resolveOrganizationTeam(apiClient, hostname, input.OwnerID, input.TeamID) if err != nil { @@ -46,6 +50,57 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a input.OwnerID = orgID } + if input.RepositoryID != "" { + var response struct { + CloneTemplateRepository struct { + Repository api.Repository + } + } + + if input.OwnerID == "" { + var err error + input.OwnerID, err = api.CurrentUserID(apiClient, hostname) + if err != nil { + return nil, err + } + } + + templateInput := repoTemplateInput{ + Name: input.Name, + Visibility: input.Visibility, + OwnerID: input.OwnerID, + RepositoryID: input.RepositoryID, + } + + variables := map[string]interface{}{ + "input": templateInput, + } + + err := apiClient.GraphQL(hostname, ` + mutation CloneTemplateRepository($input: CloneTemplateRepositoryInput!) { + cloneTemplateRepository(input: $input) { + repository { + id + name + owner { login } + url + } + } + } + `, variables, &response) + if err != nil { + return nil, err + } + + return api.InitRepoHostname(&response.CloneTemplateRepository.Repository, hostname), nil + } + + var response struct { + CreateRepository struct { + Repository api.Repository + } + } + variables := map[string]interface{}{ "input": input, } From 32e5d053282017cbbfc8620078f046eada27bf9a Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 14:58:40 -0400 Subject: [PATCH 02/18] [Fix] Warn user when --template is passed with incompatible flags --- pkg/cmd/repo/create/create.go | 5 +++++ pkg/cmd/repo/create/http.go | 1 + 2 files changed, 6 insertions(+) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 28d8384cc..57264c879 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -1,6 +1,7 @@ package create import ( + "errors" "fmt" "net/http" "path" @@ -76,6 +77,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co return runF(opts) } + if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || !opts.EnableIssues || !opts.EnableWiki) { + return &cmdutil.FlagError{Err: errors.New(`the '--template' option is not supported with '--homepage, --team, --enable-issues or --enable-wiki'`)} + } + return createRun(opts) }, } diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index f3623c6e2..e8d3f29b1 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -29,6 +29,7 @@ type repoTemplateInput struct { OwnerID string `json:"ownerId,omitempty"` RepositoryID string `json:"repositoryId,omitempty"` + Description string `json:"description,omitempty"` } // repoCreate creates a new GitHub repository From 263e3a6a356b8e062f133e7d6dd5ad9cae7766f1 Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 17:57:55 -0400 Subject: [PATCH 03/18] Update create_test.go --- pkg/cmd/repo/create/create_test.go | 91 ++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index 25f5519ab..f3de15c5a 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -303,3 +303,94 @@ func TestRepoCreate_orgWithTeam(t *testing.T) { t.Errorf("expected %q, got %q", "TEAMID", teamID) } } + +func TestRepoCreate_template(t *testing.T) { + reg := &httpmock.Registry{} + reg.Register( + httpmock.GraphQL(`mutation CloneTemplateRepository\b`), + httpmock.StringResponse(` + { "data": { "cloneTemplateRepository": { + "repository": { + "id": "REPOID", + "name": "REPO", + "owner": { + "login": "OWNER" + }, + "url": "https://github.com/OWNER/REPO" + } + } } }`)) + + reg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(` + { "data": { + "repository": { + "id": "REPOID", + "description": "DESCRIPTION" + } } }`)) + + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"ID":"OWNERID"}}}`)) + + httpClient := &http.Client{Transport: reg} + + var seenCmd *exec.Cmd + restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable { + seenCmd = cmd + return &test.OutputStub{} + }) + defer restoreCmd() + + as, surveyTearDown := prompt.InitAskStubber() + defer surveyTearDown() + + as.Stub([]*prompt.QuestionStub{ + { + Name: "repoVisibility", + Value: "PRIVATE", + }, + }) + as.Stub([]*prompt.QuestionStub{ + { + Name: "confirmSubmit", + Value: true, + }, + }) + + output, err := runCommand(httpClient, "REPO --template='OWNER/REPO'") + if err != nil { + t.Errorf("error running command `repo create`: %v", err) + } + + assert.Equal(t, "https://github.com/OWNER/REPO\n", output.String()) + assert.Equal(t, "", output.Stderr()) + + if seenCmd == nil { + t.Fatal("expected a command to run") + } + assert.Equal(t, "git remote add -f origin https://github.com/OWNER/REPO.git", strings.Join(seenCmd.Args, " ")) + + var reqBody struct { + Query string + Variables struct { + Input map[string]interface{} + } + } + + if len(reg.Requests) != 3 { + t.Fatalf("expected 3 HTTP request, got %d", len(reg.Requests)) + } + + bodyBytes, _ := ioutil.ReadAll(reg.Requests[2].Body) + _ = json.Unmarshal(bodyBytes, &reqBody) + if repoName := reqBody.Variables.Input["name"].(string); repoName != "REPO" { + t.Errorf("expected %q, got %q", "REPO", repoName) + } + if repoVisibility := reqBody.Variables.Input["visibility"].(string); repoVisibility != "PRIVATE" { + t.Errorf("expected %q, got %q", "PRIVATE", repoVisibility) + } + if ownerId := reqBody.Variables.Input["ownerId"].(string); ownerId != "OWNERID" { + t.Errorf("expected %q, got %q", "OWNERID", ownerId) + } +} From 2886dd913f1621646b960fd61d0f1672bba9b64d Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 19:13:06 -0400 Subject: [PATCH 04/18] [Refactor] toView -> toClone --- pkg/cmd/repo/create/create.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 57264c879..2ac91eb55 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -181,24 +181,23 @@ func createRun(opts *CreateOptions) error { return err } - var toView ghrepo.Interface + var toClone ghrepo.Interface apiClient := api.NewClientFromHTTP(httpClient) - // var err errors - viewURL := opts.Template - if !strings.Contains(viewURL, "/") { + cloneURL := opts.Template + if !strings.Contains(cloneURL, "/") { currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default()) if err != nil { return err } - viewURL = currentUser + "/" + viewURL + cloneURL = currentUser + "/" + cloneURL } - toView, err = ghrepo.FromFullName(viewURL) + toClone, err = ghrepo.FromFullName(cloneURL) if err != nil { return fmt.Errorf("argument error: %w", err) } - repo, err := api.GitHubRepo(apiClient, toView) + repo, err := api.GitHubRepo(apiClient, toClone) if err != nil { return err } From 99372f0dbc47cb1c67628004407909ec607d8141 Mon Sep 17 00:00:00 2001 From: Colin Shum Date: Thu, 27 Aug 2020 19:13:17 -0400 Subject: [PATCH 05/18] [Refactor] Add variadic argument to repoCreate to support templates --- pkg/cmd/repo/create/create.go | 8 +++----- pkg/cmd/repo/create/create_test.go | 2 +- pkg/cmd/repo/create/http.go | 8 +++----- pkg/cmd/repo/create/http_test.go | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 2ac91eb55..fc53b460e 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -78,7 +78,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } if opts.Template != "" && (opts.Homepage != "" || opts.Team != "" || !opts.EnableIssues || !opts.EnableWiki) { - return &cmdutil.FlagError{Err: errors.New(`the '--template' option is not supported with '--homepage, --team, --enable-issues or --enable-wiki'`)} + return &cmdutil.FlagError{Err: errors.New(`The '--template' option is not supported with '--homepage, --team, --enable-issues or --enable-wiki'`)} } return createRun(opts) @@ -173,8 +173,7 @@ func createRun(opts *CreateOptions) error { } } - // find template ID - + // Find template repo ID if opts.Template != "" { httpClient, err := opts.HttpClient() if err != nil { @@ -210,7 +209,6 @@ func createRun(opts *CreateOptions) error { Visibility: visibility, OwnerID: repoToCreate.RepoOwner(), TeamID: opts.Team, - RepositoryID: opts.Template, Description: opts.Description, HomepageURL: opts.Homepage, HasIssuesEnabled: opts.EnableIssues, @@ -231,7 +229,7 @@ func createRun(opts *CreateOptions) error { } if opts.ConfirmSubmit { - repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input) + repo, err := repoCreate(httpClient, repoToCreate.RepoHost(), input, opts.Template) if err != nil { return err } diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index f3de15c5a..8eb1a2423 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -379,7 +379,7 @@ func TestRepoCreate_template(t *testing.T) { } if len(reg.Requests) != 3 { - t.Fatalf("expected 3 HTTP request, got %d", len(reg.Requests)) + t.Fatalf("expected 3 HTTP requests, got %d", len(reg.Requests)) } bodyBytes, _ := ioutil.ReadAll(reg.Requests[2].Body) diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index e8d3f29b1..70df0dfb7 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -14,8 +14,6 @@ type repoCreateInput struct { HomepageURL string `json:"homepageUrl,omitempty"` Description string `json:"description,omitempty"` - RepositoryID string `json:"repositoryId,omitempty"` - OwnerID string `json:"ownerId,omitempty"` TeamID string `json:"teamId,omitempty"` @@ -33,7 +31,7 @@ type repoTemplateInput struct { } // repoCreate creates a new GitHub repository -func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*api.Repository, error) { +func repoCreate(client *http.Client, hostname string, input repoCreateInput, templateRepositoryID string) (*api.Repository, error) { apiClient := api.NewClientFromHTTP(client) if input.TeamID != "" { @@ -51,7 +49,7 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a input.OwnerID = orgID } - if input.RepositoryID != "" { + if templateRepositoryID != "" { var response struct { CloneTemplateRepository struct { Repository api.Repository @@ -70,7 +68,7 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a Name: input.Name, Visibility: input.Visibility, OwnerID: input.OwnerID, - RepositoryID: input.RepositoryID, + RepositoryID: templateRepositoryID, } variables := map[string]interface{}{ diff --git a/pkg/cmd/repo/create/http_test.go b/pkg/cmd/repo/create/http_test.go index 4b764572c..c8e2e7a2a 100644 --- a/pkg/cmd/repo/create/http_test.go +++ b/pkg/cmd/repo/create/http_test.go @@ -21,7 +21,7 @@ func Test_RepoCreate(t *testing.T) { HomepageURL: "http://example.com", } - _, err := repoCreate(httpClient, "github.com", input) + _, err := repoCreate(httpClient, "github.com", input, "") if err != nil { t.Fatalf("unexpected error: %v", err) } From a3eb099c14cdfcc0705ddfc464d464358f93bd79 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 31 Aug 2020 08:35:32 +0200 Subject: [PATCH 06/18] Remove feedback survey from README --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4fd0311e0..06d3279db 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,9 @@ the terminal next to where you are already working with `git` and your code. 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 +## Feedback -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 - -And if you spot bugs or have features that you'd really like to see in `gh`, please check out the [contributing page][] +We love to hear feedback about `gh`. If you spot bugs or have features that you'd really like to see in `gh`, please check out the [contributing page][]. ## Usage @@ -128,7 +124,7 @@ Install and upgrade: Install and upgrade: 1. Download the `.rpm` file from the [releases page][]; -2. Install the downloaded file: `sudo yum localinstall gh_*_linux_amd64.rpm` +2. Install the downloaded file: `sudo yum localinstall gh_*_linux_amd64.rpm` ### openSUSE/SUSE Linux From a8add832b54a6b6529c982e7801fc2f267ec304c Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 31 Aug 2020 16:27:27 +0200 Subject: [PATCH 07/18] Address PR feedback and link cleanup --- README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 06d3279db..3a1099679 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ the terminal next to where you are already working with `git` and your code. 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. -## Feedback +## We want your feedback -We love to hear feedback about `gh`. If you spot bugs or have features that you'd really like to see in `gh`, please check out the [contributing page][]. +We'd love to hear your feedback about `gh`. If you spot bugs or have features that you'd really like to see in `gh`, please open an [issue][] and check out the [contributing page][]. ## Usage @@ -23,14 +23,14 @@ We love to hear feedback about `gh`. If you spot bugs or have features that you' ## Documentation -Read the [official docs](https://cli.github.com/manual/) for more information. +Read the [official docs][] for more information. ## Comparison with hub 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 -tool. Check out our [more detailed explanation](/docs/gh-vs-hub.md) to learn more. +tool. Check out our [more detailed explanation][] to learn more. @@ -135,7 +135,7 @@ Install and upgrade: ### Arch Linux -Arch Linux users can install from the [community repo](https://www.archlinux.org/packages/community/x86_64/github-cli/): +Arch Linux users can install from the [community repo][]: ```bash pacman -S github-cli @@ -155,11 +155,15 @@ Download packaged binaries from the [releases page][]. ### Build from source -See here on how to [build GitHub CLI from source](/docs/source.md). +See here on how to [build GitHub CLI from source][]. -[docs]: https://cli.github.com/manual +[official docs]: https://cli.github.com/manual [scoop]: https://scoop.sh [Chocolatey]: https://chocolatey.org [releases page]: https://github.com/cli/cli/releases/latest [hub]: https://github.com/github/hub [contributing page]: https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md +[issue]: https://github.com/cli/cli/issues/new/choose +[more detailed explanation]: /docs/gh-vs-hub.md +[community repo]: https://www.archlinux.org/packages/community/x86_64/github-cli/ +[build GitHub CLI from source]: /docs/source.md From 766e4950d9293847a1befc3f964be1a03aac729c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 31 Aug 2020 16:46:22 +0200 Subject: [PATCH 08/18] Be transparent about which part of `pr create` flow failed When applying metadata to the new PR such as assignees or reviewers, if the operation fails, an error message would get printed: failed to create pull request: This was misleading, because the PR did get created; it's just that updating it failed. The new error message is: https://github.com/OWNER/REPO/pull/123 pull request update failed: The PR URL is printed on stdout and the error message is printed on stderr. In case of any errors, the exit code is still non-zero. --- api/queries_pr.go | 4 ++-- pkg/cmd/pr/create/create.go | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index f2b398adf..18be5e1fa 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -683,7 +683,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter } err := client.GraphQL(repo.RepoHost(), updateQuery, variables, &result) if err != nil { - return nil, err + return pr, err } } @@ -708,7 +708,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter } err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result) if err != nil { - return nil, err + return pr, err } } diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index c9e1fb6ab..6b144a5e9 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -366,11 +366,15 @@ func createRun(opts *CreateOptions) error { } pr, err := api.CreatePullRequest(client, baseRepo, params) - if err != nil { - return fmt.Errorf("failed to create pull request: %w", err) + if pr != nil { + fmt.Fprintln(opts.IO.Out, pr.URL) + } + if err != nil { + if pr != nil { + return fmt.Errorf("pull request update failed: %w", err) + } + return fmt.Errorf("pull request create failed: %w", err) } - - fmt.Fprintln(opts.IO.Out, pr.URL) } else if action == shared.PreviewAction { openURL, err := generateCompareURL(baseRepo, baseBranch, headBranchLabel, title, body, tb.Assignees, tb.Labels, tb.Projects, tb.Milestones) if err != nil { From cb4cc72e5030d358138f859babde62ab02e7b6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 31 Aug 2020 22:22:22 +0200 Subject: [PATCH 09/18] Handle HTTP 422 response to OAuth Device flow detection If HTTP 422 is encountered, assume that OAuth Device Flow is unavailable and fall back to OAuth app authorization flow. --- auth/oauth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/oauth.go b/auth/oauth.go index 3a933811c..a4ccc5a79 100644 --- a/auth/oauth.go +++ b/auth/oauth.go @@ -69,7 +69,7 @@ func (oa *OAuthFlow) ObtainAccessToken() (accessToken string, err error) { } } - if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 || + if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 || resp.StatusCode == 422 || (resp.StatusCode == 400 && values != nil && values.Get("error") == "unauthorized_client") { // OAuth Device Flow is not available; continue with OAuth browser flow with a // local server endpoint as callback target From 022d29ce796331b96650411236a0083b4d71616b Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 1 Sep 2020 09:40:27 +0200 Subject: [PATCH 10/18] Further cleanup up README links and update contributing doc --- .github/CONTRIBUTING.md | 1 + README.md | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e610380e9..573da443b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,6 +10,7 @@ We accept pull requests for bug fixes and features where we've discussed the app Please do: +* check existing issues to verify that the bug or feature request has not already been submitted * open an issue if things aren't working as expected * open an issue to propose a significant change * open a pull request to fix a bug diff --git a/README.md b/README.md index 3a1099679..01f18ab74 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ While in beta, GitHub CLI is available for repos hosted on GitHub.com only. It c ## We want your feedback -We'd love to hear your feedback about `gh`. If you spot bugs or have features that you'd really like to see in `gh`, please open an [issue][] and check out the [contributing page][]. +We'd love to hear your feedback about `gh`. If you spot bugs or have features that you'd really like to see in `gh`, please check out the [contributing page][]. ## Usage @@ -30,7 +30,7 @@ Read the [official docs][] for more information. 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 -tool. Check out our [more detailed explanation][] to learn more. +tool. Check out our [more detailed explanation][gh-vs-hub] to learn more. @@ -135,7 +135,7 @@ Install and upgrade: ### Arch Linux -Arch Linux users can install from the [community repo][]: +Arch Linux users can install from the [community repo][arch linux repo]: ```bash pacman -S github-cli @@ -155,7 +155,7 @@ Download packaged binaries from the [releases page][]. ### Build from source -See here on how to [build GitHub CLI from source][]. +See here on how to [build GitHub CLI from source][build from source]. [official docs]: https://cli.github.com/manual [scoop]: https://scoop.sh @@ -163,7 +163,6 @@ See here on how to [build GitHub CLI from source][]. [releases page]: https://github.com/cli/cli/releases/latest [hub]: https://github.com/github/hub [contributing page]: https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md -[issue]: https://github.com/cli/cli/issues/new/choose -[more detailed explanation]: /docs/gh-vs-hub.md -[community repo]: https://www.archlinux.org/packages/community/x86_64/github-cli/ -[build GitHub CLI from source]: /docs/source.md +[gh-vs-hub]: /docs/gh-vs-hub.md +[arch linux repo]: https://www.archlinux.org/packages/community/x86_64/github-cli +[build from source]: /docs/source.md From 35e739dfbd5abc6d328990b827ea5706d91b0d69 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 1 Sep 2020 10:25:16 +0200 Subject: [PATCH 11/18] Link to bugs and feature requests --- .github/CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 573da443b..a22a9ffa3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,6 +3,8 @@ [legal]: https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license [license]: ../LICENSE [code-of-conduct]: CODE-OF-CONDUCT.md +[bug issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3Abug +[feature request issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3A+label%3Aenhancement Hi! Thanks for your interest in contributing to the GitHub CLI! @@ -10,7 +12,7 @@ We accept pull requests for bug fixes and features where we've discussed the app Please do: -* check existing issues to verify that the bug or feature request has not already been submitted +* check existing issues to verify that the [bug][bug issues] or [feature request][feature request issues] has not already been submitted * open an issue if things aren't working as expected * open an issue to propose a significant change * open a pull request to fix a bug From a8b06c329bb0258ff60906532906130c9833c12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 1 Sep 2020 12:31:36 +0200 Subject: [PATCH 12/18] Ensure correct ANSI color output during OAuth flow on Windows We used to write directly to `os.Stderr`, but we first need to convert that into a colorable stream. --- internal/config/config_setup.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/config/config_setup.go b/internal/config/config_setup.go index c2ee598c1..649e105b7 100644 --- a/internal/config/config_setup.go +++ b/internal/config/config_setup.go @@ -12,6 +12,7 @@ import ( "github.com/cli/cli/auth" "github.com/cli/cli/pkg/browser" "github.com/cli/cli/utils" + "github.com/mattn/go-colorable" ) var ( @@ -29,7 +30,9 @@ func IsGitHubApp(id string) bool { } func AuthFlowWithConfig(cfg Config, hostname, notice string, additionalScopes []string) (string, error) { - token, userLogin, err := authFlow(hostname, notice, additionalScopes) + stderr := colorable.NewColorableStderr() + + token, userLogin, err := authFlow(hostname, stderr, notice, additionalScopes) if err != nil { return "", err } @@ -48,14 +51,17 @@ func AuthFlowWithConfig(cfg Config, hostname, notice string, additionalScopes [] return "", err } - AuthFlowComplete() + fmt.Fprintf(stderr, "%s Authentication complete. %s to continue...\n", + utils.GreenCheck(), utils.Bold("Press Enter")) + _ = waitForEnter(os.Stdin) + return token, nil } -func authFlow(oauthHost, notice string, additionalScopes []string) (string, string, error) { +func authFlow(oauthHost string, w io.Writer, notice string, additionalScopes []string) (string, string, error) { var verboseStream io.Writer if strings.Contains(os.Getenv("DEBUG"), "oauth") { - verboseStream = os.Stderr + verboseStream = w } minimumScopes := []string{"repo", "read:org", "gist"} @@ -73,9 +79,9 @@ func authFlow(oauthHost, notice string, additionalScopes []string) (string, stri HTTPClient: http.DefaultClient, OpenInBrowser: func(url, code string) error { if code != "" { - fmt.Fprintf(os.Stderr, "%s First copy your one-time code: %s\n", utils.Yellow("!"), utils.Bold(code)) + fmt.Fprintf(w, "%s First copy your one-time code: %s\n", utils.Yellow("!"), utils.Bold(code)) } - fmt.Fprintf(os.Stderr, "- %s to open %s in your browser... ", utils.Bold("Press Enter"), oauthHost) + fmt.Fprintf(w, "- %s to open %s in your browser... ", utils.Bold("Press Enter"), oauthHost) _ = waitForEnter(os.Stdin) browseCmd, err := browser.Command(url) @@ -84,15 +90,15 @@ func authFlow(oauthHost, notice string, additionalScopes []string) (string, stri } err = browseCmd.Run() if err != nil { - fmt.Fprintf(os.Stderr, "%s Failed opening a web browser at %s\n", utils.Red("!"), url) - fmt.Fprintf(os.Stderr, " %s\n", err) - fmt.Fprint(os.Stderr, " Please try entering the URL in your browser manually\n") + fmt.Fprintf(w, "%s Failed opening a web browser at %s\n", utils.Red("!"), url) + fmt.Fprintf(w, " %s\n", err) + fmt.Fprint(w, " Please try entering the URL in your browser manually\n") } return nil }, } - fmt.Fprintln(os.Stderr, notice) + fmt.Fprintln(w, notice) token, err := flow.ObtainAccessToken() if err != nil { @@ -107,12 +113,6 @@ func authFlow(oauthHost, notice string, additionalScopes []string) (string, stri return token, userLogin, nil } -func AuthFlowComplete() { - fmt.Fprintf(os.Stderr, "%s Authentication complete. %s to continue...\n", - utils.GreenCheck(), utils.Bold("Press Enter")) - _ = waitForEnter(os.Stdin) -} - func getViewer(hostname, token string) (string, error) { http := api.NewClient(api.AddHeader("Authorization", fmt.Sprintf("token %s", token))) return api.CurrentLoginName(http, hostname) From 1a051e9c4444fd18327fddae37687e146ab63130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 1 Sep 2020 12:59:58 +0200 Subject: [PATCH 13/18] Validate hostname as entered in the auth prompt --- pkg/cmd/auth/login/login.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 597bc910b..ce626a71b 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -156,7 +156,7 @@ func loginRun(opts *LoginOptions) error { if isEnterprise { err := prompt.SurveyAskOne(&survey.Input{ Message: "GHE hostname:", - }, &hostname, survey.WithValidator(survey.Required)) + }, &hostname, survey.WithValidator(hostnameValidator)) if err != nil { return fmt.Errorf("could not prompt: %w", err) } @@ -288,3 +288,14 @@ func loginRun(opts *LoginOptions) error { return nil } + +func hostnameValidator(v interface{}) error { + val := v.(string) + if len(strings.TrimSpace(val)) < 1 { + return errors.New("a value is required") + } + if strings.ContainsRune(val, '/') || strings.ContainsRune(val, ':') { + return errors.New("invalid hostname") + } + return nil +} From b029397d322ac7c11d49306470358e8adf685b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 1 Sep 2020 13:06:13 +0200 Subject: [PATCH 14/18] Validate the `--hostname` flag value --- pkg/cmd/auth/login/login.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index ce626a71b..d4732425f 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -87,6 +87,10 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm } } + if err := hostnameValidator(opts.Hostname); err != nil { + return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)} + } + if runF != nil { return runF(opts) } From 0de3f678bc26df282aa75bfaff5ad2fa896a9672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 1 Sep 2020 13:19:19 +0200 Subject: [PATCH 15/18] Only validate `--hostname` when flag was provided --- pkg/cmd/auth/login/login.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index d4732425f..0842b14c8 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -87,8 +87,10 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm } } - if err := hostnameValidator(opts.Hostname); err != nil { - return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)} + if cmd.Flags().Changed("hostname") { + if err := hostnameValidator(opts.Hostname); err != nil { + return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)} + } } if runF != nil { From 48a827ee34feaf7e412b7eb0dbf988652cacc116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 1 Sep 2020 13:19:30 +0200 Subject: [PATCH 16/18] Tweak `gh config set -h` output format on `auth login` --- pkg/cmd/auth/login/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 0842b14c8..07e64a6dc 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -262,7 +262,7 @@ 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) + 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 From b4956006beeb8fc553d09099c18dc256225aa056 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 1 Sep 2020 15:52:58 +0200 Subject: [PATCH 17/18] Fix up feature request issues url --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a22a9ffa3..8a1dc4849 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,7 +4,7 @@ [license]: ../LICENSE [code-of-conduct]: CODE-OF-CONDUCT.md [bug issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3Abug -[feature request issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3A+label%3Aenhancement +[feature request issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement Hi! Thanks for your interest in contributing to the GitHub CLI! From 49ab3ec5bffed7e2939d5ab9040d2d7deaa1ad67 Mon Sep 17 00:00:00 2001 From: nate smith Date: Tue, 1 Sep 2020 11:18:34 -0500 Subject: [PATCH 18/18] check for tty before creating colorables --- pkg/iostreams/iostreams.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 23f81fd44..e9b9c2220 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -103,12 +103,20 @@ func (s *IOStreams) TerminalWidth() int { } func System() *IOStreams { - return &IOStreams{ + stdoutIsTTY := isTerminal(os.Stdout) + stderrIsTTY := isTerminal(os.Stderr) + + io := &IOStreams{ In: os.Stdin, Out: colorable.NewColorable(os.Stdout), ErrOut: colorable.NewColorable(os.Stderr), - colorEnabled: os.Getenv("NO_COLOR") == "" && isTerminal(os.Stdout), + colorEnabled: os.Getenv("NO_COLOR") == "" && stdoutIsTTY, } + + // prevent duplicate isTerminal queries now that we know the answer + io.SetStdoutTTY(stdoutIsTTY) + io.SetStderrTTY(stderrIsTTY) + return io } func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {