2019-01-23 01:23:56 +03:00
|
|
|
package commands
|
|
|
|
|
|
|
|
import (
|
2019-01-24 16:36:23 +03:00
|
|
|
"fmt"
|
2019-01-23 01:23:56 +03:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2019-01-24 16:38:03 +03:00
|
|
|
"regexp"
|
2019-01-26 16:47:55 +03:00
|
|
|
"strconv"
|
2019-01-23 01:23:56 +03:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/github/hub/github"
|
|
|
|
"github.com/github/hub/ui"
|
|
|
|
"github.com/github/hub/utils"
|
|
|
|
)
|
|
|
|
|
|
|
|
var cmdApi = &Command{
|
|
|
|
Run: apiCommand,
|
2019-01-24 21:28:18 +03:00
|
|
|
Usage: "api [-t] [-X <METHOD>] [--cache <TTL>] <ENDPOINT> [-F <KEY-VALUE>]",
|
|
|
|
Long: `Low-level GitHub API request interface.
|
2019-01-23 01:23:56 +03:00
|
|
|
|
|
|
|
## Options:
|
|
|
|
-X, --method <METHOD>
|
2019-01-24 21:28:18 +03:00
|
|
|
The HTTP method to use for the request (default: "GET"). The method is
|
|
|
|
automatically set to "POST" if '--field' or '--raw-field' are used.
|
|
|
|
|
|
|
|
Use '-XGET' to force serializing fields into the query string for the GET
|
|
|
|
request instead of JSON body of the POST request.
|
|
|
|
|
2019-01-23 01:23:56 +03:00
|
|
|
-F, --field <KEY-VALUE>
|
2019-01-26 16:47:55 +03:00
|
|
|
Send data in 'KEY=VALUE' format. The <VALUE> part has some magic handling;
|
|
|
|
see '--raw-field' for passing arbitrary strings.
|
|
|
|
|
|
|
|
If <VALUE> starts with "@", the rest of the value is interpreted as a
|
|
|
|
filename to read the value from. Use "@-" to read from standard input.
|
|
|
|
|
|
|
|
If <VALUE> is "true", "false", "null", or looks like a number, an
|
|
|
|
appropriate JSON type is used instead of a string.
|
2019-01-24 21:28:18 +03:00
|
|
|
|
|
|
|
Unless '-XGET' was used, all fields are sent serialized as JSON within the
|
2019-01-26 21:08:30 +03:00
|
|
|
request body. When <ENDPOINT> is "graphql", all fields other than "query"
|
|
|
|
are grouped under "variables". See
|
|
|
|
<https://graphql.org/learn/queries/#variables>
|
2019-01-24 21:28:18 +03:00
|
|
|
|
2019-01-24 16:33:20 +03:00
|
|
|
-f, --raw-field <KEY-VALUE>
|
2019-01-26 21:08:30 +03:00
|
|
|
Same as '--field', except that it allows values starting with "@", literal
|
|
|
|
strings "true", "false", and "null", as well as strings that look like
|
|
|
|
numbers.
|
2019-01-24 21:28:18 +03:00
|
|
|
|
2019-01-28 00:05:29 +03:00
|
|
|
-H, --header <KEY-VALUE>
|
|
|
|
An HTTP request header in 'KEY: VALUE' format.
|
|
|
|
|
2019-01-28 02:36:07 +03:00
|
|
|
-i, --include
|
|
|
|
Include HTTP response headers in the output.
|
|
|
|
|
2019-01-23 01:23:56 +03:00
|
|
|
-t, --flat
|
2019-01-24 21:28:18 +03:00
|
|
|
Parse response JSON and output the data in a line-based key-value format
|
|
|
|
suitable for use in shell scripts.
|
|
|
|
|
2019-01-23 01:23:56 +03:00
|
|
|
--cache <TTL>
|
2019-01-24 21:28:18 +03:00
|
|
|
Cache successful responses to GET requests for <TTL> seconds.
|
|
|
|
|
|
|
|
When using "graphql" as <ENDPOINT>, caching will apply to responses to POST
|
|
|
|
requests as well. Just make sure to not use '--cache' for any GraphQL
|
|
|
|
mutations.
|
|
|
|
|
|
|
|
<ENDPOINT>
|
|
|
|
The GitHub API endpoint to send the HTTP request to (default: "/").
|
|
|
|
|
|
|
|
To learn about available endpoints, see <https://developer.github.com/v3/>.
|
2019-01-26 16:47:55 +03:00
|
|
|
To make GraphQL queries, use "graphql" as <ENDPOINT> and pass '-F query=QUERY'.
|
2019-01-24 21:28:18 +03:00
|
|
|
|
2019-01-26 16:47:55 +03:00
|
|
|
If the literal strings "{owner}" or "{repo}" appear in <ENDPOINT> or in the
|
|
|
|
GraphQL "query" field, fill in those placeholders with values read from the
|
|
|
|
git remote configuration of the current git repository.
|
2019-01-24 21:28:18 +03:00
|
|
|
|
|
|
|
## Examples:
|
|
|
|
|
|
|
|
# fetch information about the currently authenticated user as JSON
|
|
|
|
$ hub api user
|
|
|
|
|
|
|
|
# list user repositories as line-based output
|
|
|
|
$ hub api --flat users/octocat/repos
|
|
|
|
|
|
|
|
# post a comment to issue #23 of the current repository
|
|
|
|
$ hub api repos/{owner}/{repo}/issues/23/comments --raw-field "body=Nice job!"
|
|
|
|
|
|
|
|
# perform a GraphQL query read from a file
|
|
|
|
$ hub api graphql -F query=@path/to/myquery.graphql
|
|
|
|
|
|
|
|
## See also:
|
|
|
|
|
|
|
|
hub(1)
|
2019-01-23 01:23:56 +03:00
|
|
|
`,
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
CmdRunner.Use(cmdApi)
|
|
|
|
}
|
|
|
|
|
|
|
|
func apiCommand(cmd *Command, args *Args) {
|
|
|
|
path := ""
|
|
|
|
if !args.IsParamsEmpty() {
|
|
|
|
path = args.GetParam(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
method := "GET"
|
|
|
|
if args.Flag.HasReceived("--method") {
|
|
|
|
method = args.Flag.Value("--method")
|
2019-01-26 18:33:11 +03:00
|
|
|
} else if args.Flag.HasReceived("--field") || args.Flag.HasReceived("--raw-field") {
|
2019-01-23 01:23:56 +03:00
|
|
|
method = "POST"
|
|
|
|
}
|
|
|
|
cacheTTL := args.Flag.Int("--cache")
|
|
|
|
|
|
|
|
params := make(map[string]interface{})
|
|
|
|
for _, val := range args.Flag.AllValues("--field") {
|
|
|
|
parts := strings.SplitN(val, "=", 2)
|
|
|
|
if len(parts) >= 2 {
|
2019-01-26 16:47:55 +03:00
|
|
|
params[parts[0]] = magicValue(parts[1])
|
2019-01-24 16:33:20 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, val := range args.Flag.AllValues("--raw-field") {
|
|
|
|
parts := strings.SplitN(val, "=", 2)
|
|
|
|
if len(parts) >= 2 {
|
|
|
|
params[parts[0]] = parts[1]
|
2019-01-23 01:23:56 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-28 00:05:29 +03:00
|
|
|
headers := make(map[string]string)
|
|
|
|
for _, val := range args.Flag.AllValues("--header") {
|
|
|
|
parts := strings.SplitN(val, ":", 2)
|
|
|
|
if len(parts) >= 2 {
|
|
|
|
headers[parts[0]] = strings.TrimLeft(parts[1], " ")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-23 01:23:56 +03:00
|
|
|
host := ""
|
|
|
|
owner := ""
|
|
|
|
repo := ""
|
|
|
|
localRepo, localRepoErr := github.LocalRepo()
|
|
|
|
if localRepoErr == nil {
|
|
|
|
var project *github.Project
|
|
|
|
if project, localRepoErr = localRepo.MainProject(); localRepoErr == nil {
|
|
|
|
host = project.Host
|
|
|
|
owner = project.Owner
|
|
|
|
repo = project.Name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if host == "" {
|
2019-01-26 17:56:05 +03:00
|
|
|
defHost, err := github.CurrentConfig().DefaultHostNoPrompt()
|
2019-01-23 01:23:56 +03:00
|
|
|
utils.Check(err)
|
|
|
|
host = defHost.Host
|
|
|
|
}
|
2019-01-24 16:36:23 +03:00
|
|
|
|
|
|
|
if path == "graphql" && params["query"] != nil {
|
|
|
|
query := params["query"].(string)
|
|
|
|
query = strings.Replace(query, quote("{owner}"), quote(owner), 1)
|
|
|
|
query = strings.Replace(query, quote("{repo}"), quote(repo), 1)
|
2019-01-26 16:47:55 +03:00
|
|
|
|
|
|
|
variables := make(map[string]interface{})
|
|
|
|
for key, value := range params {
|
|
|
|
if key != "query" {
|
|
|
|
variables[key] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(variables) > 0 {
|
|
|
|
params = make(map[string]interface{})
|
|
|
|
params["variables"] = variables
|
|
|
|
}
|
|
|
|
|
2019-01-24 16:36:23 +03:00
|
|
|
params["query"] = query
|
|
|
|
} else {
|
|
|
|
path = strings.Replace(path, "{owner}", owner, 1)
|
|
|
|
path = strings.Replace(path, "{repo}", repo, 1)
|
|
|
|
}
|
2019-01-23 01:23:56 +03:00
|
|
|
|
|
|
|
gh := github.NewClient(host)
|
2019-01-28 00:05:29 +03:00
|
|
|
response, err := gh.GenericAPIRequest(method, path, params, headers, cacheTTL)
|
2019-01-23 01:23:56 +03:00
|
|
|
utils.Check(err)
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
2019-01-24 16:38:03 +03:00
|
|
|
args.NoForward()
|
|
|
|
|
2019-01-28 00:34:05 +03:00
|
|
|
out := ui.Stdout
|
|
|
|
colorize := ui.IsTerminal(os.Stdout)
|
|
|
|
success := response.StatusCode < 300
|
|
|
|
parseJSON := args.Flag.Bool("--flat")
|
|
|
|
|
|
|
|
if !success {
|
2019-01-24 16:38:03 +03:00
|
|
|
jsonType, _ := regexp.MatchString(`[/+]json(?:;|$)`, response.Header.Get("Content-Type"))
|
2019-01-28 00:34:05 +03:00
|
|
|
parseJSON = parseJSON && jsonType
|
2019-01-24 16:38:03 +03:00
|
|
|
}
|
|
|
|
|
2019-01-28 02:36:07 +03:00
|
|
|
if args.Flag.Bool("--include") {
|
|
|
|
fmt.Fprintf(out, "%s %s\r\n", response.Proto, response.Status)
|
|
|
|
response.Header.Write(out)
|
|
|
|
fmt.Fprintf(out, "\r\n")
|
|
|
|
}
|
|
|
|
|
2019-01-28 00:34:05 +03:00
|
|
|
if parseJSON {
|
|
|
|
utils.JSONPath(out, response.Body, colorize)
|
2019-01-23 01:23:56 +03:00
|
|
|
} else {
|
2019-01-28 00:34:05 +03:00
|
|
|
io.Copy(out, response.Body)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !success {
|
|
|
|
os.Exit(22)
|
2019-01-23 01:23:56 +03:00
|
|
|
}
|
|
|
|
}
|
2019-01-24 16:33:20 +03:00
|
|
|
|
2019-01-26 16:47:55 +03:00
|
|
|
const (
|
|
|
|
trueVal = "true"
|
|
|
|
falseVal = "false"
|
|
|
|
nilVal = "null"
|
|
|
|
)
|
|
|
|
|
|
|
|
func magicValue(value string) interface{} {
|
|
|
|
switch value {
|
|
|
|
case trueVal:
|
|
|
|
return true
|
|
|
|
case falseVal:
|
|
|
|
return false
|
|
|
|
case nilVal:
|
|
|
|
return nil
|
|
|
|
default:
|
|
|
|
if strings.HasPrefix(value, "@") {
|
|
|
|
return string(readFile(value[1:]))
|
|
|
|
} else if i, err := strconv.Atoi(value); err == nil {
|
|
|
|
return i
|
2019-01-24 16:33:20 +03:00
|
|
|
} else {
|
2019-01-26 16:47:55 +03:00
|
|
|
return value
|
2019-01-24 16:33:20 +03:00
|
|
|
}
|
2019-01-26 16:47:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func readFile(file string) (content []byte) {
|
|
|
|
var err error
|
|
|
|
if file == "-" {
|
|
|
|
content, err = ioutil.ReadAll(os.Stdin)
|
2019-01-24 16:33:20 +03:00
|
|
|
} else {
|
2019-01-26 16:47:55 +03:00
|
|
|
content, err = ioutil.ReadFile(file)
|
2019-01-24 16:33:20 +03:00
|
|
|
}
|
2019-01-26 16:47:55 +03:00
|
|
|
utils.Check(err)
|
|
|
|
return
|
2019-01-24 16:33:20 +03:00
|
|
|
}
|
2019-01-24 16:36:23 +03:00
|
|
|
|
|
|
|
func quote(s string) string {
|
|
|
|
return fmt.Sprintf("%q", s)
|
|
|
|
}
|