diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index da208b08a..27e26f004 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -71,8 +71,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command 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", ":repo", and ":branch" in the endpoint argument will - get replaced with values from the repository of the current directory. + Placeholder values "{owner}", "{repo}", and "{branch}" in the endpoint argument will + get replaced with values from the repository of the current directory. Note that in + some shells, for example PowerShell, you may need to enclose any value that contains + "{...}" in quotes to prevent the shell from applying special meaning to curly braces. The default HTTP request method is "GET" normally and "POST" if any parameters were added. Override the method with %[1]s--method%[1]s. @@ -87,7 +89,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command - literal values "true", "false", "null", and integer numbers get converted to appropriate JSON types; - - placeholder values ":owner", ":repo", and ":branch" get populated with values + - placeholder values "{owner}", "{repo}", and "{branch}" 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. @@ -106,10 +108,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command `, "`"), Example: heredoc.Doc(` # list releases in the current repository - $ gh api repos/:owner/:repo/releases + $ gh api repos/{owner}/{repo}/releases # post an issue comment - $ gh api repos/:owner/:repo/issues/123/comments -f body='Hi from CLI' + $ gh api repos/{owner}/{repo}/issues/123/comments -f body='Hi from CLI' # add parameters to a GET request $ gh api -X GET search/issues -f q='repo:cli/cli is:open remote' @@ -121,14 +123,14 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command $ gh api --preview baptiste,nebula ... # print only specific fields from the response - $ gh api repos/:owner/:repo/issues --jq '.[].title' + $ gh api repos/{owner}/{repo}/issues --jq '.[].title' # use a template for the output - $ gh api repos/:owner/:repo/issues --template \ + $ gh api repos/{owner}/{repo}/issues --template \ '{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " | color "yellow"}}){{"\n"}}{{end}}' # list releases with GraphQL - $ gh api graphql -F owner=':owner' -F name=':repo' -f query=' + $ gh api graphql -F owner='{owner}' -F name='{repo}' -f query=' query($name: String!, $owner: String!) { repository(owner: $owner, name: $name) { releases(last: 3) { @@ -397,41 +399,41 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream return } -var placeholderRE = regexp.MustCompile(`\:(owner|repo|branch)\b`) +var placeholderRE = regexp.MustCompile(`(\:(owner|repo|branch)\b|\{[a-z]+\})`) -// fillPlaceholders populates `:owner` and `:repo` placeholders with values from the current repository +// fillPlaceholders replaces placeholders with values from the current repository func fillPlaceholders(value string, opts *ApiOptions) (string, error) { - if !placeholderRE.MatchString(value) { - return value, nil - } + var err error + return placeholderRE.ReplaceAllStringFunc(value, func(m string) string { + var name string + if m[0] == ':' { + name = m[1:] + } else { + name = m[1 : len(m)-1] + } - baseRepo, err := opts.BaseRepo() - if err != nil { - return value, err - } - - filled := placeholderRE.ReplaceAllStringFunc(value, func(m string) string { - switch m { - case ":owner": - return baseRepo.RepoOwner() - case ":repo": - return baseRepo.RepoName() - case ":branch": - branch, e := opts.Branch() - if e != nil { + switch name { + case "owner": + if baseRepo, e := opts.BaseRepo(); e == nil { + return baseRepo.RepoOwner() + } else { + err = e + } + case "repo": + if baseRepo, e := opts.BaseRepo(); e == nil { + return baseRepo.RepoName() + } else { + err = e + } + case "branch": + if branch, e := opts.Branch(); e == nil { + return branch + } else { err = e } - return branch - default: - panic(fmt.Sprintf("invalid placeholder: %q", m)) } - }) - - if err != nil { - return value, err - } - - return filled, nil + return m + }), err } func printHeaders(w io.Writer, headers http.Header, colorize bool) { diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go index 6bab83f09..acaff19da 100644 --- a/pkg/cmd/api/api_test.go +++ b/pkg/cmd/api/api_test.go @@ -870,7 +870,7 @@ func Test_magicFieldValue(t *testing.T) { wantErr: false, }, { - name: "placeholder", + name: "placeholder colon", args: args{ v: ":owner", opts: &ApiOptions{ @@ -883,6 +883,20 @@ func Test_magicFieldValue(t *testing.T) { want: "hubot", wantErr: false, }, + { + name: "placeholder braces", + 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{ @@ -964,7 +978,7 @@ func Test_fillPlaceholders(t *testing.T) { wantErr: false, }, { - name: "has substitutes", + name: "has substitutes (colon)", args: args{ value: "repos/:owner/:repo/releases", opts: &ApiOptions{ @@ -977,39 +991,96 @@ func Test_fillPlaceholders(t *testing.T) { wantErr: false, }, { - name: "has branch placeholder", + name: "has branch placeholder (colon)", args: args{ - value: "repos/cli/cli/branches/:branch/protection/required_status_checks", + value: "repos/owner/repo/branches/:branch/protection/required_status_checks", opts: &ApiOptions{ - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("cli", "cli"), nil - }, + BaseRepo: nil, Branch: func() (string, error) { return "trunk", nil }, }, }, - want: "repos/cli/cli/branches/trunk/protection/required_status_checks", + want: "repos/owner/repo/branches/trunk/protection/required_status_checks", wantErr: false, }, { - name: "has branch placeholder and git is in detached head", + name: "has branch placeholder and git is in detached head (colon)", args: args{ value: "repos/:owner/:repo/branches/:branch", opts: &ApiOptions{ BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("cli", "cli"), nil + return ghrepo.New("hubot", "robot-uprising"), nil }, Branch: func() (string, error) { return "", git.ErrNotOnAnyBranch }, }, }, - want: "repos/:owner/:repo/branches/:branch", + want: "repos/hubot/robot-uprising/branches/:branch", wantErr: true, }, { - name: "no greedy substitutes", + name: "has substitutes", + args: args{ + value: "repos/{owner}/{repo}/releases", + opts: &ApiOptions{ + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + }, + }, + want: "repos/hubot/robot-uprising/releases", + wantErr: false, + }, + { + name: "has branch placeholder", + args: args{ + value: "repos/owner/repo/branches/{branch}/protection/required_status_checks", + opts: &ApiOptions{ + BaseRepo: nil, + Branch: func() (string, error) { + return "trunk", nil + }, + }, + }, + want: "repos/owner/repo/branches/trunk/protection/required_status_checks", + wantErr: false, + }, + { + name: "has branch placeholder and git is in detached head", + args: args{ + value: "repos/{owner}/{repo}/branches/{branch}", + opts: &ApiOptions{ + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + Branch: func() (string, error) { + return "", git.ErrNotOnAnyBranch + }, + }, + }, + want: "repos/hubot/robot-uprising/branches/{branch}", + wantErr: true, + }, + { + name: "surfaces errors in earlier placeholders", + args: args{ + value: "{branch}-{owner}", + opts: &ApiOptions{ + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("hubot", "robot-uprising"), nil + }, + Branch: func() (string, error) { + return "", git.ErrNotOnAnyBranch + }, + }, + }, + want: "{branch}-hubot", + wantErr: true, + }, + { + name: "no greedy substitutes (colon)", args: args{ value: ":ownership/:repository", opts: &ApiOptions{ @@ -1019,6 +1090,17 @@ func Test_fillPlaceholders(t *testing.T) { want: ":ownership/:repository", wantErr: false, }, + { + name: "non-placeholders are left intact", + args: args{ + value: "{}{ownership}/{repository}", + opts: &ApiOptions{ + BaseRepo: nil, + }, + }, + want: "{}{ownership}/{repository}", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {