Merge pull request #1406 from cli/migrate-gist
migrate gist create to new command format
This commit is contained in:
commit
41376a3626
11 changed files with 545 additions and 280 deletions
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
go mod verify
|
||||
go mod download
|
||||
|
||||
LINT_VERSION=1.27.0
|
||||
LINT_VERSION=1.29.0
|
||||
curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
|
||||
tar xz --strip-components 1 --wildcards \*/golangci-lint
|
||||
mkdir -p bin && mv golangci-lint bin/
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ func NewClient(opts ...ClientOption) *Client {
|
|||
return client
|
||||
}
|
||||
|
||||
// NewClientFromHTTP takes in an http.Client instance
|
||||
func NewClientFromHTTP(httpClient *http.Client) *Client {
|
||||
client := &Client{http: httpClient}
|
||||
return client
|
||||
}
|
||||
|
||||
// AddHeader turns a RoundTripper into one that adds a request header
|
||||
func AddHeader(name, value string) ClientOption {
|
||||
return func(tr http.RoundTripper) http.RoundTripper {
|
||||
|
|
@ -179,9 +185,10 @@ func (gr GraphQLErrorResponse) Error() string {
|
|||
|
||||
// HTTPError is an error returned by a failed API call
|
||||
type HTTPError struct {
|
||||
StatusCode int
|
||||
RequestURL *url.URL
|
||||
Message string
|
||||
StatusCode int
|
||||
RequestURL *url.URL
|
||||
Message string
|
||||
OAuthScopes string
|
||||
}
|
||||
|
||||
func (err HTTPError) Error() string {
|
||||
|
|
@ -322,8 +329,9 @@ func handleResponse(resp *http.Response, data interface{}) error {
|
|||
|
||||
func handleHTTPError(resp *http.Response) error {
|
||||
httpError := HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
RequestURL: resp.Request.URL,
|
||||
StatusCode: resp.StatusCode,
|
||||
RequestURL: resp.Request.URL,
|
||||
OAuthScopes: resp.Header.Get("X-Oauth-Scopes"),
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
|
|
|||
175
command/gist.go
175
command/gist.go
|
|
@ -1,175 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(gistCmd)
|
||||
gistCmd.AddCommand(gistCreateCmd)
|
||||
gistCreateCmd.Flags().StringP("desc", "d", "", "A description for this gist")
|
||||
gistCreateCmd.Flags().BoolP("public", "p", false, "List the gist publicly (default: private)")
|
||||
}
|
||||
|
||||
var gistCmd = &cobra.Command{
|
||||
Use: "gist",
|
||||
Short: "Create gists",
|
||||
Long: `Work with GitHub gists.`,
|
||||
}
|
||||
|
||||
var gistCreateCmd = &cobra.Command{
|
||||
Use: `create [<filename>... | -]`,
|
||||
Short: "Create a new gist",
|
||||
Long: `Create a new GitHub gist with given contents.
|
||||
|
||||
Gists can be created from one or multiple files. Alternatively, pass "-" as
|
||||
file name to read from standard input.
|
||||
|
||||
By default, gists are private; use '--public' to make publicly listed ones.`,
|
||||
Example: heredoc.Doc(`
|
||||
# publish file 'hello.py' as a public gist
|
||||
$ gh gist create --public hello.py
|
||||
|
||||
# create a gist with a description
|
||||
$ gh gist create hello.py -d "my Hello-World program in Python"
|
||||
|
||||
# create a gist containing several files
|
||||
$ gh gist create hello.py world.py cool.txt
|
||||
|
||||
# read from standard input to create a gist
|
||||
$ gh gist create -
|
||||
|
||||
# create a gist from output piped from another command
|
||||
$ cat cool.txt | gh gist create
|
||||
`),
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
info, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check STDIN: %w", err)
|
||||
}
|
||||
|
||||
stdinIsTTY := (info.Mode() & os.ModeCharDevice) == os.ModeCharDevice
|
||||
if stdinIsTTY {
|
||||
return &cmdutil.FlagError{Err: errors.New("no filenames passed and nothing on STDIN")}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: gistCreate,
|
||||
}
|
||||
|
||||
type Opts struct {
|
||||
Description string
|
||||
Public bool
|
||||
}
|
||||
|
||||
func gistCreate(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
client, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This performs a dummy query, checks what scopes we have, and then asks for a user to reauth
|
||||
// with expanded scopes. it introduces latency whenever this command is run: a trade-off to avoid
|
||||
// having every single user reauth as a result of this feature even if they never once use gists.
|
||||
//
|
||||
// In the future we'd rather have the ability to detect a "reauth needed" scenario and replay
|
||||
// failed requests but some short spikes indicated that that would be a fair bit of work.
|
||||
client, err = ensureScopes(ctx, client, "gist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts, err := processOpts(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand arguments: %w", err)
|
||||
}
|
||||
|
||||
fileArgs := args
|
||||
if len(args) == 0 {
|
||||
fileArgs = []string{"-"}
|
||||
}
|
||||
|
||||
files, err := processFiles(os.Stdin, fileArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect files for posting: %w", err)
|
||||
}
|
||||
|
||||
errOut := colorableErr(cmd)
|
||||
fmt.Fprintf(errOut, "%s Creating gist...\n", utils.Gray("-"))
|
||||
|
||||
gist, err := api.GistCreate(client, opts.Description, opts.Public, files)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Failed to create gist: %w", utils.Red("X"), err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(errOut, "%s Created gist\n", utils.Green("✓"))
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), gist.HTMLURL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processOpts(cmd *cobra.Command) (*Opts, error) {
|
||||
description, err := cmd.Flags().GetString("desc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
public, err := cmd.Flags().GetBool("public")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Opts{
|
||||
Description: description,
|
||||
Public: public,
|
||||
}, err
|
||||
}
|
||||
|
||||
func processFiles(stdin io.ReadCloser, filenames []string) (map[string]string, error) {
|
||||
fs := map[string]string{}
|
||||
|
||||
if len(filenames) == 0 {
|
||||
return nil, errors.New("no files passed")
|
||||
}
|
||||
|
||||
for i, f := range filenames {
|
||||
var filename string
|
||||
var content []byte
|
||||
var err error
|
||||
if f == "-" {
|
||||
filename = fmt.Sprintf("gistfile%d.txt", i)
|
||||
content, err = ioutil.ReadAll(stdin)
|
||||
if err != nil {
|
||||
return fs, fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
stdin.Close()
|
||||
} else {
|
||||
content, err = ioutil.ReadFile(f)
|
||||
if err != nil {
|
||||
return fs, fmt.Errorf("failed to read file %s: %w", f, err)
|
||||
}
|
||||
filename = path.Base(f)
|
||||
}
|
||||
|
||||
fs[filename] = string(content)
|
||||
}
|
||||
|
||||
return fs, nil
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGistCreate(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "trunk")
|
||||
|
||||
http := initFakeHTTP()
|
||||
http.Register(httpmock.REST("POST", "gists"), httpmock.StringResponse(`
|
||||
{
|
||||
"html_url": "https://gist.github.com/aa5a315d61ae9438b18d"
|
||||
}
|
||||
`))
|
||||
|
||||
output, err := RunCommand(`gist create "../test/fixtures/gistCreate.json" -d "Gist description" --public`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
|
||||
reqBody := make(map[string]interface{})
|
||||
err = json.Unmarshal(bodyBytes, &reqBody)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding JSON: %v", err)
|
||||
}
|
||||
|
||||
expectParams := map[string]interface{}{
|
||||
"description": "Gist description",
|
||||
"files": map[string]interface{}{
|
||||
"gistCreate.json": map[string]interface{}{
|
||||
"content": "{}",
|
||||
},
|
||||
},
|
||||
"public": true,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectParams, reqBody)
|
||||
assert.Equal(t, "https://gist.github.com/aa5a315d61ae9438b18d\n", output.String())
|
||||
}
|
||||
|
||||
func TestGistCreate_stdin(t *testing.T) {
|
||||
fakeStdin := strings.NewReader("hey cool how is it going")
|
||||
files, err := processFiles(ioutil.NopCloser(fakeStdin), []string{"-"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error processing files: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(files))
|
||||
assert.Equal(t, "hey cool how is it going", files["gistfile0.txt"])
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
apiCmd "github.com/cli/cli/pkg/cmd/api"
|
||||
gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/utils"
|
||||
|
|
@ -95,6 +96,14 @@ func init() {
|
|||
},
|
||||
}
|
||||
RootCmd.AddCommand(apiCmd.NewCmdApi(cmdFactory, nil))
|
||||
|
||||
gistCmd := &cobra.Command{
|
||||
Use: "gist",
|
||||
Short: "Create gists",
|
||||
Long: `Work with GitHub gists.`,
|
||||
}
|
||||
RootCmd.AddCommand(gistCmd)
|
||||
gistCmd.AddCommand(gistCreateCmd.NewCmdCreate(cmdFactory, nil))
|
||||
}
|
||||
|
||||
// RootCmd is the entry point of command-line execution
|
||||
|
|
@ -252,46 +261,6 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
|
|||
return api.NewClient(opts...), nil
|
||||
}
|
||||
|
||||
var ensureScopes = func(ctx context.Context, client *api.Client, wantedScopes ...string) (*api.Client, error) {
|
||||
hasScopes, appID, err := client.HasScopes(wantedScopes...)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
if hasScopes {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
tokenFromEnv := len(os.Getenv("GITHUB_TOKEN")) > 0
|
||||
|
||||
if config.IsGitHubApp(appID) && !tokenFromEnv && utils.IsTerminal(os.Stdin) && utils.IsTerminal(os.Stderr) {
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = config.AuthFlowWithConfig(cfg, defaultHostname, "Notice: additional authorization required")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reloadedClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
return reloadedClient, nil
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Warning: gh now requires %s OAuth scopes.\n", wantedScopes)
|
||||
fmt.Fprintf(os.Stderr, "Visit https://github.com/settings/tokens and edit your token to enable %s\n", wantedScopes)
|
||||
if tokenFromEnv {
|
||||
fmt.Fprintln(os.Stderr, "or generate a new token for the GITHUB_TOKEN environment variable")
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "or generate a new token and paste it via `gh config set -h github.com oauth_token MYTOKEN`")
|
||||
}
|
||||
return client, errors.New("Unable to reauthenticate")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func apiVerboseLog() api.ClientOption {
|
||||
logTraffic := strings.Contains(os.Getenv("DEBUG"), "api")
|
||||
colorize := utils.IsTerminal(os.Stderr)
|
||||
|
|
|
|||
|
|
@ -97,9 +97,6 @@ func initBlankContext(cfg, repo, branch string) {
|
|||
|
||||
func initFakeHTTP() *httpmock.Registry {
|
||||
http := &httpmock.Registry{}
|
||||
ensureScopes = func(ctx context.Context, client *api.Client, wantedScopes ...string) (*api.Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
apiClientForContext = func(context.Context) (*api.Client, error) {
|
||||
return api.NewClient(api.ReplaceTripper(http)), nil
|
||||
}
|
||||
|
|
|
|||
156
pkg/cmd/gist/create/create.go
Normal file
156
pkg/cmd/gist/create/create.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CreateOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
Description string
|
||||
Public bool
|
||||
Filenames []string
|
||||
|
||||
HttpClient func() (*http.Client, error)
|
||||
}
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
||||
opts := CreateOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [<filename>... | -]",
|
||||
Short: "Create a new gist",
|
||||
Long: heredoc.Doc(`
|
||||
Create a new GitHub gist with given contents.
|
||||
|
||||
Gists can be created from one or multiple files. Alternatively, pass "-" as
|
||||
file name to read from standard input.
|
||||
|
||||
By default, gists are private; use '--public' to make publicly listed ones.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
# publish file 'hello.py' as a public gist
|
||||
$ gh gist create --public hello.py
|
||||
|
||||
# create a gist with a description
|
||||
$ gh gist create hello.py -d "my Hello-World program in Python"
|
||||
|
||||
# create a gist containing several files
|
||||
$ gh gist create hello.py world.py cool.txt
|
||||
|
||||
# read from standard input to create a gist
|
||||
$ gh gist create -
|
||||
|
||||
# create a gist from output piped from another command
|
||||
$ cat cool.txt | gh gist create
|
||||
`),
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return nil
|
||||
}
|
||||
if opts.IO.IsStdinTTY() {
|
||||
return &cmdutil.FlagError{Err: errors.New("no filenames passed and nothing on STDIN")}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.HttpClient = f.HttpClient
|
||||
|
||||
opts.Filenames = args
|
||||
|
||||
if runF != nil {
|
||||
return runF(&opts)
|
||||
}
|
||||
return createRun(&opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Description, "desc", "d", "", "A description for this gist")
|
||||
cmd.Flags().BoolVarP(&opts.Public, "public", "p", false, "List the gist publicly (default: private)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createRun(opts *CreateOptions) error {
|
||||
fileArgs := opts.Filenames
|
||||
if len(fileArgs) == 0 {
|
||||
fileArgs = []string{"-"}
|
||||
}
|
||||
|
||||
files, err := processFiles(opts.IO.In, fileArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect files for posting: %w", err)
|
||||
}
|
||||
|
||||
errOut := opts.IO.ErrOut
|
||||
fmt.Fprintf(errOut, "%s Creating gist...\n", utils.Gray("-"))
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gist, err := apiCreate(httpClient, opts.Description, opts.Public, files)
|
||||
if err != nil {
|
||||
var httpError api.HTTPError
|
||||
if errors.As(err, &httpError) {
|
||||
if httpError.OAuthScopes != "" && !strings.Contains(httpError.OAuthScopes, "gist") {
|
||||
return fmt.Errorf("This command requires the 'gist' OAuth scope.\nPlease re-authenticate by doing `gh config set -h github.com oauth_token ''` and running the command again.")
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s Failed to create gist: %w", utils.Red("X"), err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(errOut, "%s Created gist\n", utils.Green("✓"))
|
||||
|
||||
fmt.Fprintln(opts.IO.Out, gist.HTMLURL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processFiles(stdin io.ReadCloser, filenames []string) (map[string]string, error) {
|
||||
fs := map[string]string{}
|
||||
|
||||
if len(filenames) == 0 {
|
||||
return nil, errors.New("no files passed")
|
||||
}
|
||||
|
||||
for i, f := range filenames {
|
||||
var filename string
|
||||
var content []byte
|
||||
var err error
|
||||
if f == "-" {
|
||||
filename = fmt.Sprintf("gistfile%d.txt", i)
|
||||
content, err = ioutil.ReadAll(stdin)
|
||||
if err != nil {
|
||||
return fs, fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
stdin.Close()
|
||||
} else {
|
||||
content, err = ioutil.ReadFile(f)
|
||||
if err != nil {
|
||||
return fs, fmt.Errorf("failed to read file %s: %w", f, err)
|
||||
}
|
||||
filename = path.Base(f)
|
||||
}
|
||||
|
||||
fs[filename] = string(content)
|
||||
}
|
||||
|
||||
return fs, nil
|
||||
}
|
||||
295
pkg/cmd/gist/create/create_test.go
Normal file
295
pkg/cmd/gist/create/create_test.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
fixtureFile = "../fixture.txt"
|
||||
)
|
||||
|
||||
func Test_processFiles(t *testing.T) {
|
||||
fakeStdin := strings.NewReader("hey cool how is it going")
|
||||
files, err := processFiles(ioutil.NopCloser(fakeStdin), []string{"-"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error processing files: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(files))
|
||||
assert.Equal(t, "hey cool how is it going", files["gistfile0.txt"])
|
||||
}
|
||||
|
||||
func TestNewCmdCreate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
factory func(*cmdutil.Factory) *cmdutil.Factory
|
||||
wants CreateOptions
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
cli: "",
|
||||
wants: CreateOptions{
|
||||
Description: "",
|
||||
Public: false,
|
||||
Filenames: []string{""},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "no arguments with TTY stdin",
|
||||
factory: func(f *cmdutil.Factory) *cmdutil.Factory {
|
||||
f.IOStreams.SetStdinTTY(true)
|
||||
return f
|
||||
},
|
||||
cli: "",
|
||||
wants: CreateOptions{
|
||||
Description: "",
|
||||
Public: false,
|
||||
Filenames: []string{""},
|
||||
},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "stdin argument",
|
||||
cli: "-",
|
||||
wants: CreateOptions{
|
||||
Description: "",
|
||||
Public: false,
|
||||
Filenames: []string{"-"},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "with description",
|
||||
cli: `-d "my new gist" -`,
|
||||
wants: CreateOptions{
|
||||
Description: "my new gist",
|
||||
Public: false,
|
||||
Filenames: []string{"-"},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "public",
|
||||
cli: `--public -`,
|
||||
wants: CreateOptions{
|
||||
Description: "",
|
||||
Public: true,
|
||||
Filenames: []string{"-"},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "list of files",
|
||||
cli: "file1.txt file2.txt",
|
||||
wants: CreateOptions{
|
||||
Description: "",
|
||||
Public: false,
|
||||
Filenames: []string{"file1.txt", "file2.txt"},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
|
||||
if tt.factory != nil {
|
||||
f = tt.factory(f)
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *CreateOptions
|
||||
cmd := NewCmdCreate(f, func(opts *CreateOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.Description, gotOpts.Description)
|
||||
assert.Equal(t, tt.wants.Public, gotOpts.Public)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *CreateOptions
|
||||
stdin string
|
||||
wantOut string
|
||||
wantStderr string
|
||||
wantParams map[string]interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "public",
|
||||
opts: &CreateOptions{
|
||||
Public: true,
|
||||
Filenames: []string{fixtureFile},
|
||||
},
|
||||
wantOut: "https://gist.github.com/aa5a315d61ae9438b18d\n",
|
||||
wantStderr: "- Creating gist...\n✓ Created gist\n",
|
||||
wantErr: false,
|
||||
wantParams: map[string]interface{}{
|
||||
"public": true,
|
||||
"files": map[string]interface{}{
|
||||
"fixture.txt": map[string]interface{}{
|
||||
"content": "{}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with description",
|
||||
opts: &CreateOptions{
|
||||
Description: "an incredibly interesting gist",
|
||||
Filenames: []string{fixtureFile},
|
||||
},
|
||||
wantOut: "https://gist.github.com/aa5a315d61ae9438b18d\n",
|
||||
wantStderr: "- Creating gist...\n✓ Created gist\n",
|
||||
wantErr: false,
|
||||
wantParams: map[string]interface{}{
|
||||
"description": "an incredibly interesting gist",
|
||||
"files": map[string]interface{}{
|
||||
"fixture.txt": map[string]interface{}{
|
||||
"content": "{}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple files",
|
||||
opts: &CreateOptions{
|
||||
Filenames: []string{fixtureFile, "-"},
|
||||
},
|
||||
stdin: "cool stdin content",
|
||||
wantOut: "https://gist.github.com/aa5a315d61ae9438b18d\n",
|
||||
wantStderr: "- Creating gist...\n✓ Created gist\n",
|
||||
wantErr: false,
|
||||
wantParams: map[string]interface{}{
|
||||
"files": map[string]interface{}{
|
||||
"fixture.txt": map[string]interface{}{
|
||||
"content": "{}",
|
||||
},
|
||||
"gistfile1.txt": map[string]interface{}{
|
||||
"content": "cool stdin content",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stdin arg",
|
||||
opts: &CreateOptions{
|
||||
Filenames: []string{"-"},
|
||||
},
|
||||
stdin: "cool stdin content",
|
||||
wantOut: "https://gist.github.com/aa5a315d61ae9438b18d\n",
|
||||
wantStderr: "- Creating gist...\n✓ Created gist\n",
|
||||
wantErr: false,
|
||||
wantParams: map[string]interface{}{
|
||||
"files": map[string]interface{}{
|
||||
"gistfile0.txt": map[string]interface{}{
|
||||
"content": "cool stdin content",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(httpmock.REST("POST", "gists"),
|
||||
httpmock.JSONResponse(struct {
|
||||
Html_url string
|
||||
}{"https://gist.github.com/aa5a315d61ae9438b18d"}))
|
||||
|
||||
mockClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
tt.opts.HttpClient = mockClient
|
||||
|
||||
io, stdin, stdout, stderr := iostreams.Test()
|
||||
tt.opts.IO = io
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
stdin.WriteString(tt.stdin)
|
||||
|
||||
if err := createRun(tt.opts); (err != nil) != tt.wantErr {
|
||||
t.Errorf("createRun() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
bodyBytes, _ := ioutil.ReadAll(reg.Requests[0].Body)
|
||||
reqBody := make(map[string]interface{})
|
||||
err := json.Unmarshal(bodyBytes, &reqBody)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding JSON: %v", err)
|
||||
}
|
||||
assert.Equal(t, tt.wantOut, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
assert.Equal(t, tt.wantParams, reqBody)
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CreateRun_reauth(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(httpmock.REST("POST", "gists"), func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 404,
|
||||
Request: req,
|
||||
Header: map[string][]string{
|
||||
"X-Oauth-Scopes": {"coolScope"},
|
||||
},
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString("oh no")),
|
||||
}, nil
|
||||
})
|
||||
|
||||
mockClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
opts := &CreateOptions{
|
||||
IO: io,
|
||||
HttpClient: mockClient,
|
||||
Filenames: []string{fixtureFile},
|
||||
}
|
||||
|
||||
err := createRun(opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected oauth error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "Please re-authenticate") {
|
||||
t.Errorf("got unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
53
pkg/cmd/gist/create/http.go
Normal file
53
pkg/cmd/gist/create/http.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
)
|
||||
|
||||
// Gist represents a GitHub's gist.
|
||||
type Gist struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Public bool `json:"public,omitempty"`
|
||||
Files map[GistFilename]GistFile `json:"files,omitempty"`
|
||||
HTMLURL string `json:"html_url,omitempty"`
|
||||
}
|
||||
|
||||
type GistFilename string
|
||||
|
||||
type GistFile struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
func apiCreate(httpClient *http.Client, description string, public bool, files map[string]string) (*Gist, error) {
|
||||
gistFiles := map[GistFilename]GistFile{}
|
||||
|
||||
for filename, content := range files {
|
||||
gistFiles[GistFilename(filename)] = GistFile{content}
|
||||
}
|
||||
|
||||
path := "gists"
|
||||
body := &Gist{
|
||||
Description: description,
|
||||
Public: public,
|
||||
Files: gistFiles,
|
||||
}
|
||||
result := Gist{}
|
||||
|
||||
requestByte, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestBody := bytes.NewReader(requestByte)
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
err = apiClient.REST("POST", path, requestBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
|
@ -16,12 +16,30 @@ type IOStreams struct {
|
|||
ErrOut io.Writer
|
||||
|
||||
colorEnabled bool
|
||||
|
||||
stdinTTYOverride bool
|
||||
stdinIsTTY bool
|
||||
}
|
||||
|
||||
func (s *IOStreams) ColorEnabled() bool {
|
||||
return s.colorEnabled
|
||||
}
|
||||
|
||||
func (s *IOStreams) SetStdinTTY(isTTY bool) {
|
||||
s.stdinTTYOverride = true
|
||||
s.stdinIsTTY = isTTY
|
||||
}
|
||||
|
||||
func (s *IOStreams) IsStdinTTY() bool {
|
||||
if s.stdinTTYOverride {
|
||||
return s.stdinIsTTY
|
||||
}
|
||||
if stdin, ok := s.In.(*os.File); ok {
|
||||
return isTerminal(stdin)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func System() *IOStreams {
|
||||
var out io.Writer = os.Stdout
|
||||
var colorEnabled bool
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue