Skip to content

Commit 4c26d61

Browse files
committed
Merge remote-tracking branch 'origin/api-template' into api-jq
2 parents 03baeb2 + eb08774 commit 4c26d61

File tree

22 files changed

+373
-60
lines changed

22 files changed

+373
-60
lines changed

cmd/gh/main.go

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
surveyCore "github.com/AlecAivazis/survey/v2/core"
16+
"github.com/AlecAivazis/survey/v2/terminal"
1617
"github.com/cli/cli/api"
1718
"github.com/cli/cli/internal/build"
1819
"github.com/cli/cli/internal/config"
@@ -32,7 +33,21 @@ import (
3233

3334
var updaterEnabled = ""
3435

36+
type exitCode int
37+
38+
const (
39+
exitOK exitCode = 0
40+
exitError exitCode = 1
41+
exitCancel exitCode = 2
42+
exitAuth exitCode = 4
43+
)
44+
3545
func main() {
46+
code := mainRun()
47+
os.Exit(int(code))
48+
}
49+
50+
func mainRun() exitCode {
3651
buildDate := build.Date
3752
buildVersion := build.Version
3853

@@ -78,7 +93,7 @@ func main() {
7893
cfg, err := cmdFactory.Config()
7994
if err != nil {
8095
fmt.Fprintf(stderr, "failed to read configuration: %s\n", err)
81-
os.Exit(2)
96+
return exitError
8297
}
8398

8499
if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" {
@@ -102,7 +117,7 @@ func main() {
102117
expandedArgs, isShell, err = expand.ExpandAlias(cfg, os.Args, nil)
103118
if err != nil {
104119
fmt.Fprintf(stderr, "failed to process aliases: %s\n", err)
105-
os.Exit(2)
120+
return exitError
106121
}
107122

108123
if hasDebug {
@@ -113,7 +128,7 @@ func main() {
113128
exe, err := safeexec.LookPath(expandedArgs[0])
114129
if err != nil {
115130
fmt.Fprintf(stderr, "failed to run external command: %s", err)
116-
os.Exit(3)
131+
return exitError
117132
}
118133

119134
externalCmd := exec.Command(exe, expandedArgs[1:]...)
@@ -125,14 +140,14 @@ func main() {
125140
err = preparedCmd.Run()
126141
if err != nil {
127142
if ee, ok := err.(*exec.ExitError); ok {
128-
os.Exit(ee.ExitCode())
143+
return exitCode(ee.ExitCode())
129144
}
130145

131146
fmt.Fprintf(stderr, "failed to run external command: %s", err)
132-
os.Exit(3)
147+
return exitError
133148
}
134149

135-
os.Exit(0)
150+
return exitOK
136151
}
137152
}
138153

@@ -142,34 +157,41 @@ func main() {
142157
fmt.Fprintln(stderr, cs.Bold("Welcome to GitHub CLI!"))
143158
fmt.Fprintln(stderr)
144159
fmt.Fprintln(stderr, "To authenticate, please run `gh auth login`.")
145-
os.Exit(4)
160+
return exitAuth
146161
}
147162

148163
rootCmd.SetArgs(expandedArgs)
149164

150165
if cmd, err := rootCmd.ExecuteC(); err != nil {
166+
if err == cmdutil.SilentError {
167+
return exitError
168+
} else if cmdutil.IsUserCancellation(err) {
169+
if errors.Is(err, terminal.InterruptErr) {
170+
// ensure the next shell prompt will start on its own line
171+
fmt.Fprint(stderr, "\n")
172+
}
173+
return exitCancel
174+
}
175+
151176
printError(stderr, err, cmd, hasDebug)
152177

153178
var httpErr api.HTTPError
154179
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
155-
fmt.Println("hint: try authenticating with `gh auth login`")
180+
fmt.Fprintln(stderr, "hint: try authenticating with `gh auth login`")
156181
}
157182

158-
os.Exit(1)
183+
return exitError
159184
}
160185
if root.HasFailed() {
161-
os.Exit(1)
186+
return exitError
162187
}
163188

164189
newRelease := <-updateMessageChan
165190
if newRelease != nil {
166-
isHomebrew := false
167-
if ghExe, err := os.Executable(); err == nil {
168-
isHomebrew = isUnderHomebrew(ghExe)
169-
}
191+
isHomebrew := isUnderHomebrew(cmdFactory.Executable)
170192
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
171193
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
172-
return
194+
return exitOK
173195
}
174196
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
175197
ansi.Color("A new release of gh is available:", "yellow"),
@@ -181,13 +203,11 @@ func main() {
181203
fmt.Fprintf(stderr, "%s\n\n",
182204
ansi.Color(newRelease.URL, "yellow"))
183205
}
206+
207+
return exitOK
184208
}
185209

186210
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
187-
if err == cmdutil.SilentError {
188-
return
189-
}
190-
191211
var dnsError *net.DNSError
192212
if errors.As(err, &dnsError) {
193213
fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)

pkg/cmd/api/api.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"strconv"
1515
"strings"
1616
"syscall"
17-
"text/template"
1817
"time"
1918

2019
"github.com/MakeNowJust/heredoc"
@@ -38,6 +37,7 @@ type ApiOptions struct {
3837
MagicFields []string
3938
RawFields []string
4039
RequestHeaders []string
40+
Previews []string
4141
ShowResponseHeaders bool
4242
Paginate bool
4343
Silent bool
@@ -124,7 +124,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
124124
$ gh api -X GET search/issues -f q='repo:cli/cli is:open remote'
125125
126126
# set a custom HTTP header
127-
$ gh api -H 'Accept: application/vnd.github.XYZ-preview+json' ...
127+
$ gh api -H 'Accept: application/vnd.github.v3.raw+json' ...
128+
129+
# opt into GitHub API previews
130+
$ gh api --preview baptiste,nebula ...
128131
129132
# print only specific fields from the response
130133
$ gh api repos/:owner/:repo/issues --filter '.[].title'
@@ -200,6 +203,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
200203
cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in `key=value` format")
201204
cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in `key=value` format")
202205
cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in `key:value` format")
206+
cmd.Flags().StringSliceVarP(&opts.Previews, "preview", "p", nil, "Opt into GitHub API previews")
203207
cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response headers in the output")
204208
cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results")
205209
cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request")
@@ -246,6 +250,10 @@ func apiRun(opts *ApiOptions) error {
246250
}
247251
}
248252

253+
if len(opts.Previews) > 0 {
254+
requestHeaders = append(requestHeaders, "Accept: "+previewNamesToMIMETypes(opts.Previews))
255+
}
256+
249257
httpClient, err := opts.HttpClient()
250258
if err != nil {
251259
return err
@@ -341,25 +349,7 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream
341349
}
342350
} else if opts.Template != "" {
343351
// TODO: reuse parsed template across pagination invocations
344-
var t *template.Template
345-
t, err = parseTemplate(opts.Template, opts.IO.ColorEnabled())
346-
if err != nil {
347-
return
348-
}
349-
350-
var jsonData []byte
351-
jsonData, err = ioutil.ReadAll(responseBody)
352-
if err != nil {
353-
return
354-
}
355-
356-
var m interface{}
357-
err = json.Unmarshal(jsonData, &m)
358-
if err != nil {
359-
return
360-
}
361-
362-
err = t.Execute(opts.IO.Out, m)
352+
err = executeTemplate(opts.IO.Out, responseBody, opts.Template, opts.IO.ColorEnabled())
363353
if err != nil {
364354
return
365355
}
@@ -570,3 +560,11 @@ func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error)
570560

571561
return bodyCopy, "", nil
572562
}
563+
564+
func previewNamesToMIMETypes(names []string) string {
565+
types := []string{fmt.Sprintf("application/vnd.github.%s-preview+json", names[0])}
566+
for _, p := range names[1:] {
567+
types = append(types, fmt.Sprintf("application/vnd.github.%s-preview", p))
568+
}
569+
return strings.Join(types, ", ")
570+
}

pkg/cmd/api/api_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func Test_NewCmdApi(t *testing.T) {
4747
Paginate: false,
4848
Silent: false,
4949
CacheTTL: 0,
50+
Template: "",
5051
},
5152
wantsErr: false,
5253
},
@@ -66,6 +67,7 @@ func Test_NewCmdApi(t *testing.T) {
6667
Paginate: false,
6768
Silent: false,
6869
CacheTTL: 0,
70+
Template: "",
6971
},
7072
wantsErr: false,
7173
},
@@ -85,6 +87,7 @@ func Test_NewCmdApi(t *testing.T) {
8587
Paginate: false,
8688
Silent: false,
8789
CacheTTL: 0,
90+
Template: "",
8891
},
8992
wantsErr: false,
9093
},
@@ -104,6 +107,7 @@ func Test_NewCmdApi(t *testing.T) {
104107
Paginate: false,
105108
Silent: false,
106109
CacheTTL: 0,
110+
Template: "",
107111
},
108112
wantsErr: false,
109113
},
@@ -123,6 +127,7 @@ func Test_NewCmdApi(t *testing.T) {
123127
Paginate: true,
124128
Silent: false,
125129
CacheTTL: 0,
130+
Template: "",
126131
},
127132
wantsErr: false,
128133
},
@@ -142,6 +147,7 @@ func Test_NewCmdApi(t *testing.T) {
142147
Paginate: false,
143148
Silent: true,
144149
CacheTTL: 0,
150+
Template: "",
145151
},
146152
wantsErr: false,
147153
},
@@ -166,6 +172,7 @@ func Test_NewCmdApi(t *testing.T) {
166172
Paginate: true,
167173
Silent: false,
168174
CacheTTL: 0,
175+
Template: "",
169176
},
170177
wantsErr: false,
171178
},
@@ -190,6 +197,7 @@ func Test_NewCmdApi(t *testing.T) {
190197
Paginate: false,
191198
Silent: false,
192199
CacheTTL: 0,
200+
Template: "",
193201
},
194202
wantsErr: false,
195203
},
@@ -214,6 +222,7 @@ func Test_NewCmdApi(t *testing.T) {
214222
Paginate: false,
215223
Silent: false,
216224
CacheTTL: 0,
225+
Template: "",
217226
},
218227
wantsErr: false,
219228
},
@@ -233,6 +242,27 @@ func Test_NewCmdApi(t *testing.T) {
233242
Paginate: false,
234243
Silent: false,
235244
CacheTTL: time.Minute * 5,
245+
Template: "",
246+
},
247+
wantsErr: false,
248+
},
249+
{
250+
name: "with template",
251+
cli: "user -t 'hello {{.name}}'",
252+
wants: ApiOptions{
253+
Hostname: "",
254+
RequestMethod: "GET",
255+
RequestMethodPassed: false,
256+
RequestPath: "user",
257+
RequestInputFile: "",
258+
RawFields: []string(nil),
259+
MagicFields: []string(nil),
260+
RequestHeaders: []string(nil),
261+
ShowResponseHeaders: false,
262+
Paginate: false,
263+
Silent: false,
264+
CacheTTL: 0,
265+
Template: "hello {{.name}}",
236266
},
237267
wantsErr: false,
238268
},
@@ -270,6 +300,7 @@ func Test_NewCmdApi(t *testing.T) {
270300
assert.Equal(t, tt.wants.Paginate, opts.Paginate)
271301
assert.Equal(t, tt.wants.Silent, opts.Silent)
272302
assert.Equal(t, tt.wants.CacheTTL, opts.CacheTTL)
303+
assert.Equal(t, tt.wants.Template, opts.Template)
273304
})
274305
}
275306
}
@@ -395,6 +426,20 @@ func Test_apiRun(t *testing.T) {
395426
stdout: "HTTP/1.1 200 Okey-dokey\nContent-Type: text/plain\r\n\r\n",
396427
stderr: ``,
397428
},
429+
{
430+
name: "output template",
431+
options: ApiOptions{
432+
Template: `{{.status}}`,
433+
},
434+
httpResponse: &http.Response{
435+
StatusCode: 200,
436+
Body: ioutil.NopCloser(bytes.NewBufferString(`{"status":"not a cat"}`)),
437+
Header: http.Header{"Content-Type": []string{"application/json"}},
438+
},
439+
err: nil,
440+
stdout: "not a cat",
441+
stderr: ``,
442+
},
398443
}
399444

400445
for _, tt := range tests {
@@ -913,6 +958,32 @@ func Test_fillPlaceholders(t *testing.T) {
913958
}
914959
}
915960

961+
func Test_previewNamesToMIMETypes(t *testing.T) {
962+
tests := []struct {
963+
name string
964+
previews []string
965+
want string
966+
}{
967+
{
968+
name: "single",
969+
previews: []string{"nebula"},
970+
want: "application/vnd.github.nebula-preview+json",
971+
},
972+
{
973+
name: "multiple",
974+
previews: []string{"nebula", "baptiste", "squirrel-girl"},
975+
want: "application/vnd.github.nebula-preview+json, application/vnd.github.baptiste-preview, application/vnd.github.squirrel-girl-preview",
976+
},
977+
}
978+
for _, tt := range tests {
979+
t.Run(tt.name, func(t *testing.T) {
980+
if got := previewNamesToMIMETypes(tt.previews); got != tt.want {
981+
t.Errorf("previewNamesToMIMETypes() = %q, want %q", got, tt.want)
982+
}
983+
})
984+
}
985+
}
986+
916987
func Test_processResponse_template(t *testing.T) {
917988
io, _, stdout, stderr := iostreams.Test()
918989

0 commit comments

Comments
 (0)