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.
This commit is contained in:
Mislav Marohnić 2020-06-11 15:00:29 +02:00
parent bf9aca834a
commit acf0046718
4 changed files with 151 additions and 1 deletions

View file

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

View file

@ -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 {

View file

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

View file

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