|
1 | 1 | package commands
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "bytes" |
4 | 5 | "fmt"
|
5 | 6 | "io"
|
6 | 7 | "io/ioutil"
|
@@ -66,12 +67,20 @@ var cmdApi = &Command{
|
66 | 67 | Parse response JSON and output the data in a line-based key-value format
|
67 | 68 | suitable for use in shell scripts.
|
68 | 69 |
|
| 70 | + --paginate |
| 71 | + Automatically request and output the next page of results until all |
| 72 | + resources have been listed. For GET requests, this follows the '<next>' |
| 73 | + resource as indicated in the "Link" response header. For GraphQL queries, |
| 74 | + this utilizes 'pageInfo' that must be present in the query; see EXAMPLES. |
| 75 | +
|
| 76 | + Note that multiple JSON documents will be output as a result. |
| 77 | +
|
69 | 78 | --color[=<WHEN>]
|
70 | 79 | Enable colored output even if stdout is not a terminal. <WHEN> can be one
|
71 | 80 | of "always" (default for '--color'), "never", or "auto" (default).
|
72 | 81 |
|
73 | 82 | --cache <TTL>
|
74 |
| - Cache successful responses to GET requests for <TTL> seconds. |
| 83 | + Cache valid responses to GET requests for <TTL> seconds. |
75 | 84 |
|
76 | 85 | When using "graphql" as <ENDPOINT>, caching will apply to responses to POST
|
77 | 86 | requests as well. Just make sure to not use '--cache' for any GraphQL
|
@@ -101,6 +110,22 @@ var cmdApi = &Command{
|
101 | 110 | # perform a GraphQL query read from a file
|
102 | 111 | $ hub api graphql -F query=@path/to/myquery.graphql
|
103 | 112 |
|
| 113 | + # perform pagination with GraphQL |
| 114 | + $ hub api --paginate graphql -f query='' |
| 115 | + query($endCursor: String) { |
| 116 | + repositoryOwner(login: "USER") { |
| 117 | + repositories(first: 100, after: $endCursor) { |
| 118 | + nodes { |
| 119 | + nameWithOwner |
| 120 | + } |
| 121 | + pageInfo { |
| 122 | + hasNextPage |
| 123 | + endCursor |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + }'' |
| 128 | +
|
104 | 129 | ## See also:
|
105 | 130 |
|
106 | 131 | hub(1)
|
@@ -165,7 +190,8 @@ func apiCommand(cmd *Command, args *Args) {
|
165 | 190 | host = defHost.Host
|
166 | 191 | }
|
167 | 192 |
|
168 |
| - if path == "graphql" && params["query"] != nil { |
| 193 | + isGraphQL := path == "graphql" |
| 194 | + if isGraphQL && params["query"] != nil { |
169 | 195 | query := params["query"].(string)
|
170 | 196 | query = strings.Replace(query, quote("{owner}"), quote(owner), 1)
|
171 | 197 | query = strings.Replace(query, quote("{repo}"), quote(repo), 1)
|
@@ -203,36 +229,69 @@ func apiCommand(cmd *Command, args *Args) {
|
203 | 229 | }
|
204 | 230 |
|
205 | 231 | gh := github.NewClient(host)
|
206 |
| - response, err := gh.GenericAPIRequest(method, path, body, headers, cacheTTL) |
207 |
| - utils.Check(err) |
208 |
| - |
209 |
| - args.NoForward() |
210 | 232 |
|
211 | 233 | out := ui.Stdout
|
212 | 234 | colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color"))
|
213 |
| - success := response.StatusCode < 300 |
214 | 235 | parseJSON := args.Flag.Bool("--flat")
|
| 236 | + includeHeaders := args.Flag.Bool("--include") |
| 237 | + paginate := args.Flag.Bool("--paginate") |
215 | 238 |
|
216 |
| - if !success { |
217 |
| - jsonType, _ := regexp.MatchString(`[/+]json(?:;|$)`, response.Header.Get("Content-Type")) |
218 |
| - parseJSON = parseJSON && jsonType |
219 |
| - } |
| 239 | + args.NoForward() |
220 | 240 |
|
221 |
| - if args.Flag.Bool("--include") { |
222 |
| - fmt.Fprintf(out, "%s %s\r\n", response.Proto, response.Status) |
223 |
| - response.Header.Write(out) |
224 |
| - fmt.Fprintf(out, "\r\n") |
225 |
| - } |
| 241 | + requestLoop := true |
| 242 | + for requestLoop { |
| 243 | + response, err := gh.GenericAPIRequest(method, path, body, headers, cacheTTL) |
| 244 | + utils.Check(err) |
| 245 | + success := response.StatusCode < 300 |
226 | 246 |
|
227 |
| - if parseJSON { |
228 |
| - utils.JSONPath(out, response.Body, colorize) |
229 |
| - } else { |
230 |
| - io.Copy(out, response.Body) |
231 |
| - } |
232 |
| - response.Body.Close() |
| 247 | + jsonType := true |
| 248 | + if !success { |
| 249 | + jsonType, _ = regexp.MatchString(`[/+]json(?:;|$)`, response.Header.Get("Content-Type")) |
| 250 | + } |
| 251 | + |
| 252 | + if includeHeaders { |
| 253 | + fmt.Fprintf(out, "%s %s\r\n", response.Proto, response.Status) |
| 254 | + response.Header.Write(out) |
| 255 | + fmt.Fprintf(out, "\r\n") |
| 256 | + } |
| 257 | + |
| 258 | + endCursor := "" |
| 259 | + hasNextPage := false |
| 260 | + |
| 261 | + if parseJSON && jsonType { |
| 262 | + hasNextPage, endCursor = utils.JSONPath(out, response.Body, colorize) |
| 263 | + } else if paginate && isGraphQL { |
| 264 | + bodyCopy := &bytes.Buffer{} |
| 265 | + io.Copy(out, io.TeeReader(response.Body, bodyCopy)) |
| 266 | + hasNextPage, endCursor = utils.JSONPath(ioutil.Discard, bodyCopy, false) |
| 267 | + } else { |
| 268 | + io.Copy(out, response.Body) |
| 269 | + } |
| 270 | + response.Body.Close() |
| 271 | + |
| 272 | + if !success { |
| 273 | + os.Exit(22) |
| 274 | + } |
233 | 275 |
|
234 |
| - if !success { |
235 |
| - os.Exit(22) |
| 276 | + requestLoop = false |
| 277 | + if paginate { |
| 278 | + if isGraphQL && hasNextPage && endCursor != "" { |
| 279 | + if v, ok := params["variables"]; ok { |
| 280 | + variables := v.(map[string]interface{}) |
| 281 | + variables["endCursor"] = endCursor |
| 282 | + } else { |
| 283 | + variables := map[string]interface{}{"endCursor": endCursor} |
| 284 | + params["variables"] = variables |
| 285 | + } |
| 286 | + requestLoop = true |
| 287 | + } else if nextLink := response.Link("next"); nextLink != "" { |
| 288 | + path = nextLink |
| 289 | + requestLoop = true |
| 290 | + } |
| 291 | + } |
| 292 | + if requestLoop && !parseJSON { |
| 293 | + fmt.Fprintf(out, "\n") |
| 294 | + } |
236 | 295 | }
|
237 | 296 | }
|
238 | 297 |
|
|
0 commit comments