Repository: cli/cli
Pull request #3077 (2021-03-03 19:16:41)

Add the `api --preview` flag to opt into GitHub API previews
This was previously available manually via the `-H` flag, but it was verbose, especially when opting into multiple previews.

Before:
```sh
gh api -H 'Accept: application/vnd.github.nebula-preview+json, application/vnd.github.baptiste-preview, application/vnd.github.squirrel-girl-preview'
```

Now:
```
gh api -p nebula,baptiste -p squirrel-girl
```

I feel like this shorter version lends itself really well to usage via aliases.

Questions:
- [x] Is "preview" fine, or too ambiguous of a word in this context?
- [x] Is `-p` fine as a shorthand or would we want to use it for something else in the near future?


Patch:
diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go
index 8273b3635..7427d05b6 100644
--- a/pkg/cmd/api/api.go
+++ b/pkg/cmd/api/api.go
@@ -37,6 +37,7 @@ type ApiOptions struct {
 	MagicFields         []string
 	RawFields           []string
 	RequestHeaders      []string
+	Previews            []string
 	ShowResponseHeaders bool
 	Paginate            bool
 	Silent              bool
@@ -106,7 +107,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
 			$ gh api -X GET search/issues -f q='repo:cli/cli is:open remote'
 
 			# set a custom HTTP header
-			$ gh api -H 'Accept: application/vnd.github.XYZ-preview+json' ...
+			$ gh api -H 'Accept: application/vnd.github.v3.raw+json' ...
+
+			# opt into GitHub API previews
+			$ gh api --preview baptiste,nebula ...
 
 			# list releases with GraphQL
 			$ gh api graphql -F owner=':owner' -F name=':repo' -f query='
@@ -175,6 +179,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
 	cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in `key=value` format")
 	cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in `key=value` format")
 	cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in `key:value` format")
+	cmd.Flags().StringSliceVarP(&opts.Previews, "preview", "p", nil, "Opt into GitHub API previews")
 	cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response headers in the output")
 	cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results")
 	cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request")
@@ -219,6 +224,10 @@ func apiRun(opts *ApiOptions) error {
 		}
 	}
 
+	if len(opts.Previews) > 0 {
+		requestHeaders = append(requestHeaders, "Accept: "+previewNamesToMIMETypes(opts.Previews))
+	}
+
 	httpClient, err := opts.HttpClient()
 	if err != nil {
 		return err
@@ -513,3 +522,11 @@ func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error)
 
 	return bodyCopy, "", nil
 }
+
+func previewNamesToMIMETypes(names []string) string {
+	types := []string{fmt.Sprintf("application/vnd.github.%s-preview+json", names[0])}
+	for _, p := range names[1:] {
+		types = append(types, fmt.Sprintf("application/vnd.github.%s-preview", p))
+	}
+	return strings.Join(types, ", ")
+}


Test patch:
diff --git a/pkg/cmd/api/api_test.go b/pkg/cmd/api/api_test.go
index 69ffa100d..44582cac8 100644
--- a/pkg/cmd/api/api_test.go
+++ b/pkg/cmd/api/api_test.go
@@ -910,3 +910,29 @@ func Test_fillPlaceholders(t *testing.T) {
 		})
 	}
 }
+
+func Test_previewNamesToMIMETypes(t *testing.T) {
+	tests := []struct {
+		name     string
+		previews []string
+		want     string
+	}{
+		{
+			name:     "single",
+			previews: []string{"nebula"},
+			want:     "application/vnd.github.nebula-preview+json",
+		},
+		{
+			name:     "multiple",
+			previews: []string{"nebula", "baptiste", "squirrel-girl"},
+			want:     "application/vnd.github.nebula-preview+json, application/vnd.github.baptiste-preview, application/vnd.github.squirrel-girl-preview",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := previewNamesToMIMETypes(tt.previews); got != tt.want {
+				t.Errorf("previewNamesToMIMETypes() = %q, want %q", got, tt.want)
+			}
+		})
+	}
+}