diff --git a/pkg/cmd/gist/clone/clone.go b/pkg/cmd/gist/clone/clone.go new file mode 100644 index 000000000..8bdc932ae --- /dev/null +++ b/pkg/cmd/gist/clone/clone.go @@ -0,0 +1,101 @@ +package clone + +import ( + "fmt" + "net/http" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/git" + "github.com/cli/cli/internal/config" + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type CloneOptions struct { + HttpClient func() (*http.Client, error) + Config func() (config.Config, error) + IO *iostreams.IOStreams + + GitArgs []string + Directory string + Gist string +} + +func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Command { + opts := &CloneOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + Config: f.Config, + } + + cmd := &cobra.Command{ + DisableFlagsInUseLine: true, + + Use: "clone [] [-- ...]", + Args: cmdutil.MinimumArgs(1, "cannot clone: gist argument required"), + Short: "Clone a gist locally", + Long: heredoc.Doc(` + Clone a GitHub gist locally. + + A gist can be supplied as argument in either of the following formats: + - by ID, e.g. 5b0e0062eb8e9654adad7bb1d81cc75f + - by URL, e.g. "https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f" + + Pass additional 'git clone' flags by listing them after '--'. + `), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Gist = args[0] + opts.GitArgs = args[1:] + + if runF != nil { + return runF(opts) + } + + return cloneRun(opts) + }, + } + + cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { + if err == pflag.ErrHelp { + return err + } + return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)} + }) + + return cmd +} + +func cloneRun(opts *CloneOptions) error { + gistURL := opts.Gist + + if !git.IsURL(gistURL) { + cfg, err := opts.Config() + if err != nil { + return err + } + hostname := ghinstance.OverridableDefault() + protocol, err := cfg.Get(hostname, "git_protocol") + if err != nil { + return err + } + gistURL = formatRemoteURL(hostname, gistURL, protocol) + } + + _, err := git.RunClone(gistURL, opts.GitArgs) + if err != nil { + return err + } + + return nil +} + +func formatRemoteURL(hostname string, gistID string, protocol string) string { + if protocol == "ssh" { + return fmt.Sprintf("git@gist.%s:%s.git", hostname, gistID) + } + + return fmt.Sprintf("https://gist.%s/%s.git", hostname, gistID) +} diff --git a/pkg/cmd/gist/clone/clone_test.go b/pkg/cmd/gist/clone/clone_test.go new file mode 100644 index 000000000..8faf3d40c --- /dev/null +++ b/pkg/cmd/gist/clone/clone_test.go @@ -0,0 +1,118 @@ +package clone + +import ( + "net/http" + "strings" + "testing" + + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/test" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func runCloneCommand(httpClient *http.Client, cli string) (*test.CmdOut, error) { + io, stdin, stdout, stderr := iostreams.Test() + fac := &cmdutil.Factory{ + IOStreams: io, + HttpClient: func() (*http.Client, error) { + return httpClient, nil + }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, + } + + cmd := NewCmdClone(fac, nil) + + argv, err := shlex.Split(cli) + cmd.SetArgs(argv) + + cmd.SetIn(stdin) + cmd.SetOut(stdout) + cmd.SetErr(stderr) + + if err != nil { + panic(err) + } + + _, err = cmd.ExecuteC() + + if err != nil { + return nil, err + } + + return &test.CmdOut{OutBuf: stdout, ErrBuf: stderr}, nil +} + +func Test_GistClone(t *testing.T) { + tests := []struct { + name string + args string + want string + }{ + { + name: "shorthand", + args: "GIST", + want: "git clone https://gist.github.com/GIST.git", + }, + { + name: "shorthand with directory", + args: "GIST target_directory", + want: "git clone https://gist.github.com/GIST.git target_directory", + }, + { + name: "clone arguments", + args: "GIST -- -o upstream --depth 1", + want: "git clone -o upstream --depth 1 https://gist.github.com/GIST.git", + }, + { + name: "clone arguments with directory", + args: "GIST target_directory -- -o upstream --depth 1", + want: "git clone -o upstream --depth 1 https://gist.github.com/GIST.git target_directory", + }, + { + name: "HTTPS URL", + args: "https://gist.github.com/OWNER/GIST", + want: "git clone https://gist.github.com/OWNER/GIST", + }, + { + name: "SSH URL", + args: "git@gist.github.com:GIST.git", + want: "git clone git@gist.github.com:GIST.git", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + + httpClient := &http.Client{Transport: reg} + + cs, restore := test.InitCmdStubber() + defer restore() + + cs.Stub("") // git clone + + output, err := runCloneCommand(httpClient, tt.args) + if err != nil { + t.Fatalf("error running command `gist clone`: %v", err) + } + + assert.Equal(t, "", output.String()) + assert.Equal(t, "", output.Stderr()) + assert.Equal(t, 1, cs.Count) + assert.Equal(t, tt.want, strings.Join(cs.Calls[0].Args, " ")) + reg.Verify(t) + }) + } +} + +func Test_GistClone_flagError(t *testing.T) { + _, err := runCloneCommand(nil, "--depth 1 GIST") + if err == nil || err.Error() != "unknown flag: --depth\nSeparate git clone flags with '--'." { + t.Errorf("unexpected error %v", err) + } +} diff --git a/pkg/cmd/gist/gist.go b/pkg/cmd/gist/gist.go index 0abfc8591..df7e0f575 100644 --- a/pkg/cmd/gist/gist.go +++ b/pkg/cmd/gist/gist.go @@ -2,6 +2,7 @@ package gist import ( "github.com/MakeNowJust/heredoc" + gistCloneCmd "github.com/cli/cli/pkg/cmd/gist/clone" gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" gistDeleteCmd "github.com/cli/cli/pkg/cmd/gist/delete" gistEditCmd "github.com/cli/cli/pkg/cmd/gist/edit" @@ -26,6 +27,7 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command { }, } + cmd.AddCommand(gistCloneCmd.NewCmdClone(f, nil)) cmd.AddCommand(gistCreateCmd.NewCmdCreate(f, nil)) cmd.AddCommand(gistListCmd.NewCmdList(f, nil)) cmd.AddCommand(gistViewCmd.NewCmdView(f, nil))