mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
feat(api): add --verbose to show the request and response headers
--verbose prints the method, URL without query and headers of each request, then the status and headers of its response, on stderr like curl -v. Credentials in Authorization, Proxy-Authorization and Cookie are hidden. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
2 files changed
+53
-9
No files matched your search
+36
-9
@@ -149,7 +149,7 @@ $ tea api -X PATCH repos/alice/project --input settings.json`
|
||||
func (a *application) apiCommand() *cobra.Command {
|
||||
var method, input, output string
|
||||
var raw, typed, headerValues []string
|
||||
var include, paginate, silent bool
|
||||
var include, verbose, paginate, silent bool
|
||||
var maxPages int
|
||||
cmd := &cobra.Command{Use: "api <endpoint>", Short: "Make an authenticated Gitea API request", Long: apiLong, Example: apiExamples, Annotations: map[string]string{"tea.text": "api returns raw responses", "tea.rawjson": "true"}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||
usage := func(err error) error { return clierr.Wrap(clierr.Usage, err) }
|
||||
@@ -257,8 +257,13 @@ func (a *application) apiCommand() *cobra.Command {
|
||||
response.color = response.tty && a.colorEnabled()
|
||||
}
|
||||
return (services.API{Client: target.client}).Visit(cmd.Context(), services.APIOptions{Endpoint: endpoint, Method: method, Body: body, Headers: headers, Paginate: paginate, MaxPages: maxPages}, func(r *http.Response) error {
|
||||
if include {
|
||||
if err := writeResponseHead(cmd.ErrOrStderr(), r); err != nil {
|
||||
if verbose {
|
||||
if err := writeRequestHead(cmd.ErrOrStderr(), r.Request); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if include || verbose {
|
||||
if err := writeResponseHead(cmd.ErrOrStderr(), r, verbose); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -277,6 +282,7 @@ func (a *application) apiCommand() *cobra.Command {
|
||||
cmd.Flags().StringArrayVarP(&typed, "field", "F", nil, "Add a typed field: `name=value`; @FILE or @- reads a file or stdin")
|
||||
cmd.Flags().StringVar(&output, "output-file", "", "Write the complete response to a new `file`")
|
||||
cmd.Flags().BoolVarP(&include, "include", "i", false, "Print the response status and headers to stderr")
|
||||
cmd.Flags().BoolVar(&verbose, "verbose", false, "Print the request and the response status and headers to stderr")
|
||||
cmd.Flags().BoolVar(&silent, "silent", false, "Do not print the response body")
|
||||
cmd.Flags().BoolVar(&paginate, "paginate", false, "Follow next-page links and print every page")
|
||||
cmd.Flags().IntVar(&maxPages, "max-pages", 1000, "Stop --paginate after this many pages")
|
||||
@@ -337,13 +343,34 @@ func (l *lastByte) Write(p []byte) (int, error) {
|
||||
return l.w.Write(p)
|
||||
}
|
||||
|
||||
// writeResponseHead prints the status line and the sorted headers of r, then a blank line.
|
||||
func writeResponseHead(w io.Writer, r *http.Response) error {
|
||||
// writeResponseHead prints the status line and the sorted headers of r, then a blank line; verbose
|
||||
// marks each line with "< " like curl.
|
||||
func writeResponseHead(w io.Writer, r *http.Response, verbose bool) error {
|
||||
prefix := ""
|
||||
if verbose {
|
||||
prefix = "< "
|
||||
}
|
||||
return writeHead(w, prefix, r.Proto+" "+r.Status, r.Header)
|
||||
}
|
||||
|
||||
// writeRequestHead prints the method, URL without query and headers of r marked with "> ", hiding
|
||||
// credentials.
|
||||
func writeRequestHead(w io.Writer, r *http.Request) error {
|
||||
headers := r.Header.Clone()
|
||||
for _, name := range []string{"Authorization", "Proxy-Authorization", "Cookie"} {
|
||||
if headers.Get(name) != "" {
|
||||
headers.Set(name, "(hidden)")
|
||||
}
|
||||
}
|
||||
return writeHead(w, "> ", r.Method+" "+r.URL.Scheme+"://"+r.URL.Host+r.URL.EscapedPath(), headers)
|
||||
}
|
||||
|
||||
func writeHead(w io.Writer, prefix, first string, headers http.Header) error {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s %s\n", r.Proto, r.Status)
|
||||
for _, key := range slices.Sorted(maps.Keys(r.Header)) {
|
||||
for _, value := range r.Header.Values(key) {
|
||||
fmt.Fprintf(&b, "%s: %s\n", key, value)
|
||||
b.WriteString(prefix + first + "\n")
|
||||
for _, key := range slices.Sorted(maps.Keys(headers)) {
|
||||
for _, value := range headers.Values(key) {
|
||||
b.WriteString(prefix + terminal.SafeLine(key+": "+value) + "\n")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
@@ -265,6 +265,23 @@ func TestAPIFiltersAndFormatsJSONResponses(t *testing.T) {
|
||||
requireUsageBeforeRequests(t, "api returns raw responses; --json is not supported", "api", "version", "--json")
|
||||
}
|
||||
|
||||
func TestAPIVerboseShowsRequestAndResponseWithoutCredentials(t *testing.T) {
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("X-Tea-Test", "response")
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
deps, out, diagnostics := serverDeps(t, server.URL)
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"api", "version?access_token=secret", "--verbose", "-H", "X-Request: one"}))
|
||||
require.Equal(t, "{}", out.String())
|
||||
head := diagnostics.String()
|
||||
for _, want := range []string{"> GET " + server.URL + "/api/v1/version\n", "> Authorization: (hidden)\n", "> X-Request: one\n", "< HTTP/1.1 200 OK\n", "< X-Tea-Test: response\n"} {
|
||||
require.Contains(t, head, want)
|
||||
}
|
||||
require.NotContains(t, head, "secret")
|
||||
require.NotContains(t, head, "token token")
|
||||
}
|
||||
|
||||
func TestAPIHelpShowsJQ(t *testing.T) {
|
||||
deps, out, _ := testDeps(t)
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"api", "--help"}))
|
||||
|
||||
Reference in new issue
Block a user