X Tutup
Skip to content

Commit db74ea0

Browse files
author
Nate Smith
authored
Merge pull request cli#1165 from cli/api-repo-placeholders
api command: support `{owner}` and `{repo}` placeholders
2 parents 420d527 + 3f6d0bf commit db74ea0

File tree

4 files changed

+159
-15
lines changed

4 files changed

+159
-15
lines changed

command/root.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,22 @@ func init() {
7676
HttpClient: func() (*http.Client, error) {
7777
token := os.Getenv("GITHUB_TOKEN")
7878
if len(token) == 0 {
79+
// TODO: decouple from `context`
7980
ctx := context.New()
8081
var err error
82+
// TODO: pass IOStreams to this so that the auth flow knows if it's interactive or not
8183
token, err = ctx.AuthToken()
8284
if err != nil {
8385
return nil, err
8486
}
8587
}
8688
return httpClient(token), nil
8789
},
90+
BaseRepo: func() (ghrepo.Interface, error) {
91+
// TODO: decouple from `context`
92+
ctx := context.New()
93+
return ctx.BaseRepo()
94+
},
8895
}
8996
RootCmd.AddCommand(apiCmd.NewCmdApi(cmdFactory, nil))
9097
}

pkg/cmd/api/api.go

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
"github.com/MakeNowJust/heredoc"
17+
"github.com/cli/cli/internal/ghrepo"
1618
"github.com/cli/cli/pkg/cmdutil"
1719
"github.com/cli/cli/pkg/iostreams"
1820
"github.com/cli/cli/pkg/jsoncolor"
@@ -32,40 +34,60 @@ type ApiOptions struct {
3234
ShowResponseHeaders bool
3335

3436
HttpClient func() (*http.Client, error)
37+
BaseRepo func() (ghrepo.Interface, error)
3538
}
3639

3740
func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command {
3841
opts := ApiOptions{
3942
IO: f.IOStreams,
4043
HttpClient: f.HttpClient,
44+
BaseRepo: f.BaseRepo,
4145
}
4246

4347
cmd := &cobra.Command{
4448
Use: "api <endpoint>",
4549
Short: "Make an authenticated GitHub API request",
4650
Long: `Makes an authenticated HTTP request to the GitHub API and prints the response.
4751
48-
The <endpoint> argument should either be a path of a GitHub API v3 endpoint, or
52+
The endpoint argument should either be a path of a GitHub API v3 endpoint, or
4953
"graphql" to access the GitHub API v4.
5054
55+
Placeholder values ":owner" and ":repo" in the endpoint argument will get replaced
56+
with values from the repository of the current directory.
57+
5158
The default HTTP request method is "GET" normally and "POST" if any parameters
5259
were added. Override the method with '--method'.
5360
54-
Pass one or more '--raw-field' values in "<key>=<value>" format to add
61+
Pass one or more '--raw-field' values in "key=value" format to add
5562
JSON-encoded string parameters to the POST body.
5663
5764
The '--field' flag behaves like '--raw-field' with magic type conversion based
5865
on the format of the value:
5966
6067
- literal values "true", "false", "null", and integer numbers get converted to
6168
appropriate JSON types;
69+
- placeholder values ":owner" and ":repo" get populated with values from the
70+
repository of the current directory;
6271
- if the value starts with "@", the rest of the value is interpreted as a
6372
filename to read the value from. Pass "-" to read from standard input.
6473
6574
Raw request body may be passed from the outside via a file specified by '--input'.
6675
Pass "-" to read from standard input. In this mode, parameters specified via
6776
'--field' flags are serialized into URL query parameters.
6877
`,
78+
Example: heredoc.Doc(`
79+
$ gh api repos/:owner/:repo/releases
80+
81+
$ gh api graphql -F owner=':owner' -F name=':repo' -f query='
82+
query($name: String!, $owner: String!) {
83+
repository(owner: $owner, name: $name) {
84+
releases(last: 3) {
85+
nodes { tagName }
86+
}
87+
}
88+
}
89+
'
90+
`),
6991
Args: cobra.ExactArgs(1),
7092
RunE: func(c *cobra.Command, args []string) error {
7193
opts.RequestPath = args[0]
@@ -93,8 +115,11 @@ func apiRun(opts *ApiOptions) error {
93115
return err
94116
}
95117

118+
requestPath, err := fillPlaceholders(opts.RequestPath, opts)
119+
if err != nil {
120+
return fmt.Errorf("unable to expand placeholder in path: %w", err)
121+
}
96122
method := opts.RequestMethod
97-
requestPath := opts.RequestPath
98123
requestHeaders := opts.RequestHeaders
99124
var requestBody interface{} = params
100125

@@ -170,6 +195,33 @@ func apiRun(opts *ApiOptions) error {
170195
return nil
171196
}
172197

198+
var placeholderRE = regexp.MustCompile(`\:(owner|repo)\b`)
199+
200+
// fillPlaceholders populates `:owner` and `:repo` placeholders with values from the current repository
201+
func fillPlaceholders(value string, opts *ApiOptions) (string, error) {
202+
if !placeholderRE.MatchString(value) {
203+
return value, nil
204+
}
205+
206+
baseRepo, err := opts.BaseRepo()
207+
if err != nil {
208+
return value, err
209+
}
210+
211+
value = placeholderRE.ReplaceAllStringFunc(value, func(m string) string {
212+
switch m {
213+
case ":owner":
214+
return baseRepo.RepoOwner()
215+
case ":repo":
216+
return baseRepo.RepoName()
217+
default:
218+
panic(fmt.Sprintf("invalid placeholder: %q", m))
219+
}
220+
})
221+
222+
return value, nil
223+
}
224+
173225
func printHeaders(w io.Writer, headers http.Header, colorize bool) {
174226
var names []string
175227
for name := range headers {
@@ -204,7 +256,7 @@ func parseFields(opts *ApiOptions) (map[string]interface{}, error) {
204256
if err != nil {
205257
return params, err
206258
}
207-
value, err := magicFieldValue(strValue, opts.IO.In)
259+
value, err := magicFieldValue(strValue, opts)
208260
if err != nil {
209261
return params, fmt.Errorf("error parsing %q value: %w", key, err)
210262
}
@@ -221,9 +273,9 @@ func parseField(f string) (string, string, error) {
221273
return f[0:idx], f[idx+1:], nil
222274
}
223275

224-
func magicFieldValue(v string, stdin io.ReadCloser) (interface{}, error) {
276+
func magicFieldValue(v string, opts *ApiOptions) (interface{}, error) {
225277
if strings.HasPrefix(v, "@") {
226-
return readUserFile(v[1:], stdin)
278+
return readUserFile(v[1:], opts.IO.In)
227279
}
228280

229281
if n, err := strconv.Atoi(v); err == nil {
@@ -238,7 +290,7 @@ func magicFieldValue(v string, stdin io.ReadCloser) (interface{}, error) {
238290
case "null":
239291
return nil, nil
240292
default:
241-
return v, nil
293+
return fillPlaceholders(v, opts)
242294
}
243295
}
244296

pkg/cmd/api/api_test.go

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package api
33
import (
44
"bytes"
55
"fmt"
6-
"io"
76
"io/ioutil"
87
"net/http"
98
"os"
109
"testing"
1110

11+
"github.com/cli/cli/internal/ghrepo"
1212
"github.com/cli/cli/pkg/cmdutil"
1313
"github.com/cli/cli/pkg/iostreams"
1414
"github.com/google/shlex"
@@ -366,9 +366,11 @@ func Test_magicFieldValue(t *testing.T) {
366366
f.Close()
367367
t.Cleanup(func() { os.Remove(f.Name()) })
368368

369+
io, _, _, _ := iostreams.Test()
370+
369371
type args struct {
370-
v string
371-
stdin io.ReadCloser
372+
v string
373+
opts *ApiOptions
372374
}
373375
tests := []struct {
374376
name string
@@ -401,21 +403,41 @@ func Test_magicFieldValue(t *testing.T) {
401403
wantErr: false,
402404
},
403405
{
404-
name: "file",
405-
args: args{v: "@" + f.Name()},
406+
name: "placeholder",
407+
args: args{
408+
v: ":owner",
409+
opts: &ApiOptions{
410+
IO: io,
411+
BaseRepo: func() (ghrepo.Interface, error) {
412+
return ghrepo.New("hubot", "robot-uprising"), nil
413+
},
414+
},
415+
},
416+
want: "hubot",
417+
wantErr: false,
418+
},
419+
{
420+
name: "file",
421+
args: args{
422+
v: "@" + f.Name(),
423+
opts: &ApiOptions{IO: io},
424+
},
406425
want: []byte("file contents"),
407426
wantErr: false,
408427
},
409428
{
410-
name: "file error",
411-
args: args{v: "@"},
429+
name: "file error",
430+
args: args{
431+
v: "@",
432+
opts: &ApiOptions{IO: io},
433+
},
412434
want: nil,
413435
wantErr: true,
414436
},
415437
}
416438
for _, tt := range tests {
417439
t.Run(tt.name, func(t *testing.T) {
418-
got, err := magicFieldValue(tt.args.v, tt.args.stdin)
440+
got, err := magicFieldValue(tt.args.v, tt.args.opts)
419441
if (err != nil) != tt.wantErr {
420442
t.Errorf("magicFieldValue() error = %v, wantErr %v", err, tt.wantErr)
421443
return
@@ -451,3 +473,64 @@ func Test_openUserFile(t *testing.T) {
451473
assert.Equal(t, int64(13), length)
452474
assert.Equal(t, "file contents", string(fb))
453475
}
476+
477+
func Test_fillPlaceholders(t *testing.T) {
478+
type args struct {
479+
value string
480+
opts *ApiOptions
481+
}
482+
tests := []struct {
483+
name string
484+
args args
485+
want string
486+
wantErr bool
487+
}{
488+
{
489+
name: "no changes",
490+
args: args{
491+
value: "repos/owner/repo/releases",
492+
opts: &ApiOptions{
493+
BaseRepo: nil,
494+
},
495+
},
496+
want: "repos/owner/repo/releases",
497+
wantErr: false,
498+
},
499+
{
500+
name: "has substitutes",
501+
args: args{
502+
value: "repos/:owner/:repo/releases",
503+
opts: &ApiOptions{
504+
BaseRepo: func() (ghrepo.Interface, error) {
505+
return ghrepo.New("hubot", "robot-uprising"), nil
506+
},
507+
},
508+
},
509+
want: "repos/hubot/robot-uprising/releases",
510+
wantErr: false,
511+
},
512+
{
513+
name: "no greedy substitutes",
514+
args: args{
515+
value: ":ownership/:repository",
516+
opts: &ApiOptions{
517+
BaseRepo: nil,
518+
},
519+
},
520+
want: ":ownership/:repository",
521+
wantErr: false,
522+
},
523+
}
524+
for _, tt := range tests {
525+
t.Run(tt.name, func(t *testing.T) {
526+
got, err := fillPlaceholders(tt.args.value, tt.args.opts)
527+
if (err != nil) != tt.wantErr {
528+
t.Errorf("fillPlaceholders() error = %v, wantErr %v", err, tt.wantErr)
529+
return
530+
}
531+
if got != tt.want {
532+
t.Errorf("fillPlaceholders() got = %v, want %v", got, tt.want)
533+
}
534+
})
535+
}
536+
}

pkg/cmdutil/factory.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package cmdutil
33
import (
44
"net/http"
55

6+
"github.com/cli/cli/internal/ghrepo"
67
"github.com/cli/cli/pkg/iostreams"
78
)
89

910
type Factory struct {
1011
IOStreams *iostreams.IOStreams
1112
HttpClient func() (*http.Client, error)
13+
BaseRepo func() (ghrepo.Interface, error)
1214
}

0 commit comments

Comments
 (0)
X Tutup