Add verbose flag to api cmd (#7826)

This commit is contained in:
Jun Nishimura 2023-08-26 01:37:37 +09:00 committed by GitHub
parent fd7f987e58
commit 508065b72d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 120 additions and 117 deletions

View file

@ -23,6 +23,7 @@ type HTTPClientOptions struct {
EnableCache bool
Log io.Writer
LogColorize bool
LogVerboseHTTP bool
SkipAcceptHeaders bool
}
@ -35,10 +36,15 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
LogIgnoreEnv: true,
}
if debugEnabled, debugValue := utils.IsDebugEnabled(); debugEnabled {
debugEnabled, debugValue := utils.IsDebugEnabled()
if strings.Contains(debugValue, "api") {
opts.LogVerboseHTTP = true
}
if opts.LogVerboseHTTP || debugEnabled {
clientOpts.Log = opts.Log
clientOpts.LogColorize = opts.LogColorize
clientOpts.LogVerboseHTTP = strings.Contains(debugValue, "api")
clientOpts.LogVerboseHTTP = opts.LogVerboseHTTP
}
headers := map[string]string{

View file

@ -6,7 +6,6 @@ import (
"io"
"net/http"
"net/http/httptest"
"os"
"regexp"
"strings"
"testing"
@ -19,16 +18,14 @@ import (
func TestNewHTTPClient(t *testing.T) {
type args struct {
config tokenGetter
appVersion string
setAccept bool
config tokenGetter
appVersion string
setAccept bool
logVerboseHTTP bool
}
tests := []struct {
name string
args args
envDebug string
setGhDebug bool
envGhDebug string
host string
wantHeader map[string]string
wantStderr string
@ -36,9 +33,10 @@ func TestNewHTTPClient(t *testing.T) {
{
name: "github.com with Accept header",
args: args{
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
logVerboseHTTP: false,
},
host: "github.com",
wantHeader: map[string]string{
@ -51,9 +49,10 @@ func TestNewHTTPClient(t *testing.T) {
{
name: "github.com no Accept header",
args: args{
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: false,
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: false,
logVerboseHTTP: false,
},
host: "github.com",
wantHeader: map[string]string{
@ -66,9 +65,10 @@ func TestNewHTTPClient(t *testing.T) {
{
name: "github.com no authentication token",
args: args{
config: tinyConfig{"example.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
config: tinyConfig{"example.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
logVerboseHTTP: false,
},
host: "github.com",
wantHeader: map[string]string{
@ -81,45 +81,12 @@ func TestNewHTTPClient(t *testing.T) {
{
name: "github.com in verbose mode",
args: args{
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
logVerboseHTTP: true,
},
host: "github.com",
envDebug: "api",
setGhDebug: false,
wantHeader: map[string]string{
"authorization": "token MYTOKEN",
"user-agent": "GitHub CLI v1.2.3",
"accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview",
},
wantStderr: heredoc.Doc(`
* Request at <time>
* Request to http://<host>:<port>
> GET / HTTP/1.1
> Host: github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token
> Content-Type: application/json; charset=utf-8
> Time-Zone: <timezone>
> User-Agent: GitHub CLI v1.2.3
< HTTP/1.1 204 No Content
< Date: <time>
* Request took <duration>
`),
},
{
name: "github.com in verbose mode",
args: args{
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
appVersion: "v1.2.3",
setAccept: true,
},
host: "github.com",
envGhDebug: "api",
setGhDebug: true,
host: "github.com",
wantHeader: map[string]string{
"authorization": "token MYTOKEN",
"user-agent": "GitHub CLI v1.2.3",
@ -168,19 +135,13 @@ func TestNewHTTPClient(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("DEBUG", tt.envDebug)
if tt.setGhDebug {
t.Setenv("GH_DEBUG", tt.envGhDebug)
} else {
os.Unsetenv("GH_DEBUG")
}
ios, _, _, stderr := iostreams.Test()
client, err := NewHTTPClient(HTTPClientOptions{
AppVersion: tt.args.appVersion,
Config: tt.args.config,
Log: ios.ErrOut,
SkipAcceptHeaders: !tt.args.setAccept,
LogVerboseHTTP: tt.args.logVerboseHTTP,
})
require.NoError(t, err)

View file

@ -30,7 +30,12 @@ import (
)
type ApiOptions struct {
IO *iostreams.IOStreams
AppVersion string
BaseRepo func() (ghrepo.Interface, error)
Branch func() (string, error)
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Hostname string
RequestMethod string
@ -47,20 +52,16 @@ type ApiOptions struct {
Template string
CacheTTL time.Duration
FilterOutput string
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
BaseRepo func() (ghrepo.Interface, error)
Branch func() (string, error)
Verbose bool
}
func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command {
opts := ApiOptions{
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
AppVersion: f.AppVersion,
BaseRepo: f.BaseRepo,
Branch: f.Branch,
Config: f.Config,
IO: f.IOStreams,
}
cmd := &cobra.Command{
@ -209,7 +210,8 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
}
if err := cmdutil.MutuallyExclusive(
"only one of `--template`, `--jq`, or `--silent` may be used",
"only one of `--template`, `--jq`, `--silent`, or `--verbose` may be used",
opts.Verbose,
opts.Silent,
opts.FilterOutput != "",
opts.Template != "",
@ -237,6 +239,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"")
cmd.Flags().StringVarP(&opts.FilterOutput, "jq", "q", "", "Query to select values from the response using jq syntax")
cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"")
cmd.Flags().BoolVar(&opts.Verbose, "verbose", false, "Include full HTTP request and response in the output")
return cmd
}
@ -283,12 +286,33 @@ func apiRun(opts *ApiOptions) error {
requestHeaders = append(requestHeaders, "Accept: "+previewNamesToMIMETypes(opts.Previews))
}
httpClient, err := opts.HttpClient()
cfg, err := opts.Config()
if err != nil {
return err
}
if opts.CacheTTL > 0 {
httpClient = api.NewCachedHTTPClient(httpClient, opts.CacheTTL)
if opts.HttpClient == nil {
opts.HttpClient = func() (*http.Client, error) {
log := opts.IO.ErrOut
if opts.Verbose {
log = opts.IO.Out
}
opts := api.HTTPClientOptions{
AppVersion: opts.AppVersion,
CacheTTL: opts.CacheTTL,
Config: cfg.Authentication(),
EnableCache: opts.CacheTTL > 0,
Log: log,
LogColorize: opts.IO.ColorEnabled(),
LogVerboseHTTP: opts.Verbose,
SkipAcceptHeaders: true,
}
return api.NewHTTPClient(opts)
}
}
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
if !opts.Silent {
@ -304,10 +328,10 @@ func apiRun(opts *ApiOptions) error {
if opts.Silent {
bodyWriter = io.Discard
}
cfg, err := opts.Config()
if err != nil {
return err
if opts.Verbose {
// httpClient handles output when verbose flag is specified.
bodyWriter = io.Discard
headersWriter = io.Discard
}
host, _ := cfg.Authentication().DefaultHost()

View file

@ -52,6 +52,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -73,6 +74,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -94,6 +96,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -115,6 +118,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -136,6 +140,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -157,6 +162,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -183,6 +189,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -209,6 +216,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -235,6 +243,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -256,6 +265,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: time.Minute * 5,
Template: "",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -277,6 +287,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "hello {{.name}}",
FilterOutput: "",
Verbose: false,
},
wantsErr: false,
},
@ -298,6 +309,7 @@ func Test_NewCmdApi(t *testing.T) {
CacheTTL: 0,
Template: "",
FilterOutput: ".name",
Verbose: false,
},
wantsErr: false,
},
@ -316,6 +328,28 @@ func Test_NewCmdApi(t *testing.T) {
cli: "user --jq .foo -t '{{.foo}}'",
wantsErr: true,
},
{
name: "with verbose",
cli: "user --verbose",
wants: ApiOptions{
Hostname: "",
RequestMethod: "GET",
RequestMethodPassed: false,
RequestPath: "user",
RequestInputFile: "",
RawFields: []string(nil),
MagicFields: []string(nil),
RequestHeaders: []string(nil),
ShowResponseHeaders: false,
Paginate: false,
Silent: false,
CacheTTL: 0,
Template: "",
FilterOutput: "",
Verbose: true,
},
wantsErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -352,6 +386,7 @@ func Test_NewCmdApi(t *testing.T) {
assert.Equal(t, tt.wants.CacheTTL, opts.CacheTTL)
assert.Equal(t, tt.wants.Template, opts.Template)
assert.Equal(t, tt.wants.FilterOutput, opts.FilterOutput)
assert.Equal(t, tt.wants.Verbose, opts.Verbose)
})
}
}

View file

@ -25,6 +25,7 @@ var ssoURLRE = regexp.MustCompile(`\burl=([^;]+)`)
func New(appVersion string) *cmdutil.Factory {
f := &cmdutil.Factory{
AppVersion: appVersion,
Config: configFunc(), // No factory dependencies
ExecutableName: "gh",
}

View file

@ -2,12 +2,10 @@ package root
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
actionsCmd "github.com/cli/cli/v2/pkg/cmd/actions"
aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
@ -150,13 +148,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
cmd.AddCommand(workflowCmd.NewCmdWorkflow(&repoResolvingCmdFactory))
cmd.AddCommand(labelCmd.NewCmdLabel(&repoResolvingCmdFactory))
cmd.AddCommand(cacheCmd.NewCmdCache(&repoResolvingCmdFactory))
// the `api` command should not inherit any extra HTTP headers
bareHTTPCmdFactory := *f
bareHTTPCmdFactory.HttpClient = bareHTTPClient(f, version)
bareHTTPCmdFactory.BaseRepo = factory.SmartBaseRepoFunc(&bareHTTPCmdFactory)
cmd.AddCommand(apiCmd.NewCmdApi(&bareHTTPCmdFactory, nil))
cmd.AddCommand(apiCmd.NewCmdApi(&repoResolvingCmdFactory, nil))
// Help topics
var referenceCmd *cobra.Command
@ -222,20 +214,3 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
referenceCmd.SetHelpFunc(longPager(f.IOStreams))
return cmd, nil
}
func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, error) {
return func() (*http.Client, error) {
cfg, err := f.Config()
if err != nil {
return nil, err
}
opts := api.HTTPClientOptions{
AppVersion: version,
Config: cfg.Authentication(),
Log: f.IOStreams.ErrOut,
LogColorize: f.IOStreams.ColorEnabled(),
SkipAcceptHeaders: true,
}
return api.NewHTTPClient(opts)
}
}

View file

@ -17,19 +17,20 @@ import (
)
type Factory struct {
IOStreams *iostreams.IOStreams
Prompter prompter.Prompter
Browser browser.Browser
GitClient *git.Client
HttpClient func() (*http.Client, error)
BaseRepo func() (ghrepo.Interface, error)
Remotes func() (context.Remotes, error)
Config func() (config.Config, error)
Branch func() (string, error)
AppVersion string
ExecutableName string
Browser browser.Browser
ExtensionManager extensions.ExtensionManager
ExecutableName string
GitClient *git.Client
IOStreams *iostreams.IOStreams
Prompter prompter.Prompter
BaseRepo func() (ghrepo.Interface, error)
Branch func() (string, error)
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
Remotes func() (context.Remotes, error)
}
// Executable is the path to the currently invoked binary