Skip to content

Commit 326fe37

Browse files
committed
add gist clone command
This adds the ability to clone a gist. Usage: ```sh $ gh gist clone 5b0e0062eb8e9654adad7bb1d81cc75f $ gh gist clone https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f ``` This closes cli#2115.
1 parent f415245 commit 326fe37

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed

pkg/cmd/gist/clone/clone.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package clone
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/MakeNowJust/heredoc"
8+
"github.com/cli/cli/git"
9+
"github.com/cli/cli/internal/config"
10+
"github.com/cli/cli/internal/ghinstance"
11+
"github.com/cli/cli/pkg/cmdutil"
12+
"github.com/cli/cli/pkg/iostreams"
13+
"github.com/spf13/cobra"
14+
"github.com/spf13/pflag"
15+
)
16+
17+
type CloneOptions struct {
18+
HttpClient func() (*http.Client, error)
19+
Config func() (config.Config, error)
20+
IO *iostreams.IOStreams
21+
22+
GitArgs []string
23+
Directory string
24+
Gist string
25+
}
26+
27+
func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Command {
28+
opts := &CloneOptions{
29+
IO: f.IOStreams,
30+
HttpClient: f.HttpClient,
31+
Config: f.Config,
32+
}
33+
34+
cmd := &cobra.Command{
35+
DisableFlagsInUseLine: true,
36+
37+
Use: "clone <gist> [<directory>] [-- <gitflags>...]",
38+
Args: cmdutil.MinimumArgs(1, "cannot clone: gist argument required"),
39+
Short: "Clone a gist locally",
40+
Long: heredoc.Doc(`
41+
Clone a GitHub gist locally.
42+
43+
A gist can be supplied as argument in either of the following formats:
44+
- by ID, e.g. 5b0e0062eb8e9654adad7bb1d81cc75f
45+
- by URL, e.g. "https://gist.github.com/OWNER/5b0e0062eb8e9654adad7bb1d81cc75f"
46+
47+
Pass additional 'git clone' flags by listing them after '--'.
48+
`),
49+
RunE: func(cmd *cobra.Command, args []string) error {
50+
opts.Gist = args[0]
51+
opts.GitArgs = args[1:]
52+
53+
if runF != nil {
54+
return runF(opts)
55+
}
56+
57+
return cloneRun(opts)
58+
},
59+
}
60+
61+
cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
62+
if err == pflag.ErrHelp {
63+
return err
64+
}
65+
return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)}
66+
})
67+
68+
return cmd
69+
}
70+
71+
func cloneRun(opts *CloneOptions) error {
72+
gistURL := opts.Gist
73+
74+
if !git.IsURL(gistURL) {
75+
cfg, err := opts.Config()
76+
if err != nil {
77+
return err
78+
}
79+
hostname := ghinstance.OverridableDefault()
80+
protocol, err := cfg.Get(hostname, "git_protocol")
81+
if err != nil {
82+
return err
83+
}
84+
gistURL = formatRemoteURL(hostname, gistURL, protocol)
85+
}
86+
87+
_, err := git.RunClone(gistURL, opts.GitArgs)
88+
if err != nil {
89+
return err
90+
}
91+
92+
return nil
93+
}
94+
95+
func formatRemoteURL(hostname string, gistID string, protocol string) string {
96+
if protocol == "ssh" {
97+
return fmt.Sprintf("git@gist.%s:%s.git", hostname, gistID)
98+
}
99+
100+
return fmt.Sprintf("https://gist.%s/%s.git", hostname, gistID)
101+
}

pkg/cmd/gist/clone/clone_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package clone
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"testing"
7+
8+
"github.com/cli/cli/internal/config"
9+
"github.com/cli/cli/pkg/cmdutil"
10+
"github.com/cli/cli/pkg/httpmock"
11+
"github.com/cli/cli/pkg/iostreams"
12+
"github.com/cli/cli/test"
13+
"github.com/google/shlex"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func runCloneCommand(httpClient *http.Client, cli string) (*test.CmdOut, error) {
18+
io, stdin, stdout, stderr := iostreams.Test()
19+
fac := &cmdutil.Factory{
20+
IOStreams: io,
21+
HttpClient: func() (*http.Client, error) {
22+
return httpClient, nil
23+
},
24+
Config: func() (config.Config, error) {
25+
return config.NewBlankConfig(), nil
26+
},
27+
}
28+
29+
cmd := NewCmdClone(fac, nil)
30+
31+
argv, err := shlex.Split(cli)
32+
cmd.SetArgs(argv)
33+
34+
cmd.SetIn(stdin)
35+
cmd.SetOut(stdout)
36+
cmd.SetErr(stderr)
37+
38+
if err != nil {
39+
panic(err)
40+
}
41+
42+
_, err = cmd.ExecuteC()
43+
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return &test.CmdOut{OutBuf: stdout, ErrBuf: stderr}, nil
49+
}
50+
51+
func Test_GistClone(t *testing.T) {
52+
tests := []struct {
53+
name string
54+
args string
55+
want string
56+
}{
57+
{
58+
name: "shorthand",
59+
args: "GIST",
60+
want: "git clone https://gist.github.com/GIST.git",
61+
},
62+
{
63+
name: "shorthand with directory",
64+
args: "GIST target_directory",
65+
want: "git clone https://gist.github.com/GIST.git target_directory",
66+
},
67+
{
68+
name: "clone arguments",
69+
args: "GIST -- -o upstream --depth 1",
70+
want: "git clone -o upstream --depth 1 https://gist.github.com/GIST.git",
71+
},
72+
{
73+
name: "clone arguments with directory",
74+
args: "GIST target_directory -- -o upstream --depth 1",
75+
want: "git clone -o upstream --depth 1 https://gist.github.com/GIST.git target_directory",
76+
},
77+
{
78+
name: "HTTPS URL",
79+
args: "https://gist.github.com/OWNER/GIST",
80+
want: "git clone https://gist.github.com/OWNER/GIST",
81+
},
82+
{
83+
name: "SSH URL",
84+
args: "git@gist.github.com:GIST.git",
85+
want: "git clone git@gist.github.com:GIST.git",
86+
},
87+
}
88+
for _, tt := range tests {
89+
t.Run(tt.name, func(t *testing.T) {
90+
reg := &httpmock.Registry{}
91+
92+
httpClient := &http.Client{Transport: reg}
93+
94+
cs, restore := test.InitCmdStubber()
95+
defer restore()
96+
97+
cs.Stub("") // git clone
98+
99+
output, err := runCloneCommand(httpClient, tt.args)
100+
if err != nil {
101+
t.Fatalf("error running command `gist clone`: %v", err)
102+
}
103+
104+
assert.Equal(t, "", output.String())
105+
assert.Equal(t, "", output.Stderr())
106+
assert.Equal(t, 1, cs.Count)
107+
assert.Equal(t, tt.want, strings.Join(cs.Calls[0].Args, " "))
108+
reg.Verify(t)
109+
})
110+
}
111+
}
112+
113+
func Test_GistClone_flagError(t *testing.T) {
114+
_, err := runCloneCommand(nil, "--depth 1 GIST")
115+
if err == nil || err.Error() != "unknown flag: --depth\nSeparate git clone flags with '--'." {
116+
t.Errorf("unexpected error %v", err)
117+
}
118+
}

pkg/cmd/gist/gist.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gist
22

33
import (
44
"github.com/MakeNowJust/heredoc"
5+
gistCloneCmd "github.com/cli/cli/pkg/cmd/gist/clone"
56
gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create"
67
gistDeleteCmd "github.com/cli/cli/pkg/cmd/gist/delete"
78
gistEditCmd "github.com/cli/cli/pkg/cmd/gist/edit"
@@ -26,6 +27,7 @@ func NewCmdGist(f *cmdutil.Factory) *cobra.Command {
2627
},
2728
}
2829

30+
cmd.AddCommand(gistCloneCmd.NewCmdClone(f, nil))
2931
cmd.AddCommand(gistCreateCmd.NewCmdCreate(f, nil))
3032
cmd.AddCommand(gistListCmd.NewCmdList(f, nil))
3133
cmd.AddCommand(gistViewCmd.NewCmdView(f, nil))

0 commit comments

Comments
 (0)