Merge pull request #3536 from rneatherway/rneatherway/placeholder-syntax

Support standard path variable replacement syntax
This commit is contained in:
Mislav Marohnić 2021-04-30 14:29:10 +02:00 committed by GitHub
commit 011e455b73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 133 additions and 49 deletions

View file

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

View file

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