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] 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) }