Skip to content

Commit 25b150a

Browse files
authored
Merge pull request cli#4159 from cli/ext-create
Add extension create command
2 parents 5a328c3 + 5756e23 commit 25b150a

File tree

6 files changed

+216
-1
lines changed

6 files changed

+216
-1
lines changed

pkg/cmd/extension/command.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,43 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
141141
return nil
142142
},
143143
},
144+
&cobra.Command{
145+
Use: "create <name>",
146+
Short: "Create a new extension",
147+
Args: cmdutil.ExactArgs(1, "must specify a name for the extension"),
148+
RunE: func(cmd *cobra.Command, args []string) error {
149+
extName := args[0]
150+
if !strings.HasPrefix(extName, "gh-") {
151+
extName = "gh-" + extName
152+
}
153+
if err := m.Create(extName); err != nil {
154+
return err
155+
}
156+
if !io.IsStdoutTTY() {
157+
return nil
158+
}
159+
link := "https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions"
160+
cs := io.ColorScheme()
161+
out := heredoc.Docf(`
162+
%[1]s Created directory %[2]s
163+
%[1]s Initialized git repository
164+
%[1]s Set up extension scaffolding
165+
166+
%[2]s is ready for development
167+
168+
Install locally with: cd %[2]s && gh extension install .
169+
170+
Publish to GitHub with: gh repo create %[2]s
171+
172+
For more information on writing extensions:
173+
%[3]s
174+
`, cs.SuccessIcon(), extName, link)
175+
fmt.Fprint(io.Out, out)
176+
return nil
177+
},
178+
},
144179
)
145180

146-
extCmd.Hidden = true
147181
return &extCmd
148182
}
149183

pkg/cmd/extension/command_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"testing"
99

10+
"github.com/MakeNowJust/heredoc"
1011
"github.com/cli/cli/internal/config"
1112
"github.com/cli/cli/pkg/cmdutil"
1213
"github.com/cli/cli/pkg/extensions"
@@ -221,6 +222,51 @@ func TestNewCmdExtension(t *testing.T) {
221222
},
222223
wantStdout: "gh test\tcli/gh-test\t\ngh test2\tcli/gh-test2\tUpgrade available\n",
223224
},
225+
{
226+
name: "create extension tty",
227+
args: []string{"create", "test"},
228+
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
229+
em.CreateFunc = func(name string) error {
230+
return nil
231+
}
232+
return func(t *testing.T) {
233+
calls := em.CreateCalls()
234+
assert.Equal(t, 1, len(calls))
235+
assert.Equal(t, "gh-test", calls[0].Name)
236+
}
237+
},
238+
isTTY: true,
239+
wantStdout: heredoc.Doc(`
240+
✓ Created directory gh-test
241+
✓ Initialized git repository
242+
✓ Set up extension scaffolding
243+
244+
gh-test is ready for development
245+
246+
Install locally with: cd gh-test && gh extension install .
247+
248+
Publish to GitHub with: gh repo create gh-test
249+
250+
For more information on writing extensions:
251+
https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
252+
`),
253+
},
254+
{
255+
name: "create extension notty",
256+
args: []string{"create", "gh-test"},
257+
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
258+
em.CreateFunc = func(name string) error {
259+
return nil
260+
}
261+
return func(t *testing.T) {
262+
calls := em.CreateCalls()
263+
assert.Equal(t, 1, len(calls))
264+
assert.Equal(t, "gh-test", calls[0].Name)
265+
}
266+
},
267+
isTTY: false,
268+
wantStdout: "",
269+
},
224270
}
225271

226272
for _, tt := range tests {

pkg/cmd/extension/manager.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"runtime"
1414
"strings"
1515

16+
"github.com/MakeNowJust/heredoc"
1617
"github.com/cli/cli/internal/config"
1718
"github.com/cli/cli/pkg/extensions"
1819
"github.com/cli/cli/pkg/findsh"
@@ -249,6 +250,76 @@ func (m *Manager) installDir() string {
249250
return filepath.Join(m.dataDir(), "extensions")
250251
}
251252

253+
func (m *Manager) Create(name string) error {
254+
exe, err := m.lookPath("git")
255+
if err != nil {
256+
return err
257+
}
258+
259+
err = os.Mkdir(name, 0755)
260+
if err != nil {
261+
return err
262+
}
263+
264+
initCmd := m.newCommand(exe, "init", "--quiet", name)
265+
err = initCmd.Run()
266+
if err != nil {
267+
return err
268+
}
269+
270+
fileTmpl := heredoc.Docf(`
271+
#!/bin/bash
272+
set -e
273+
274+
echo "Hello %[1]s!"
275+
276+
# Snippets to help get started:
277+
278+
# Determine if an executable is in the PATH
279+
# if ! type -p ruby >/dev/null; then
280+
# echo "Ruby not found on the system" >&2
281+
# exit 1
282+
# fi
283+
284+
# Pass arguments through to another command
285+
# gh issue list "$@" -R cli/cli
286+
287+
# Using the gh api command to retrieve and format information
288+
# QUERY='
289+
# query($endCursor: String) {
290+
# viewer {
291+
# repositories(first: 100, after: $endCursor) {
292+
# nodes {
293+
# nameWithOwner
294+
# stargazerCount
295+
# }
296+
# }
297+
# }
298+
# }
299+
# '
300+
# TEMPLATE='
301+
# {{- range $repo := .data.viewer.repositories.nodes -}}
302+
# {{- printf "name: %[2]s - stargazers: %[3]s\n" $repo.nameWithOwner $repo.stargazerCount -}}
303+
# {{- end -}}
304+
# '
305+
# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}"
306+
`, name, "%s", "%v")
307+
filePath := filepath.Join(name, name)
308+
err = ioutil.WriteFile(filePath, []byte(fileTmpl), 0755)
309+
if err != nil {
310+
return err
311+
}
312+
313+
wd, err := os.Getwd()
314+
if err != nil {
315+
return err
316+
}
317+
dir := filepath.Join(wd, name)
318+
addCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", name, "--chmod=+x")
319+
err = addCmd.Run()
320+
return err
321+
}
322+
252323
func runCmds(cmds []*exec.Cmd, stdout, stderr io.Writer) error {
253324
for _, cmd := range cmds {
254325
cmd.Stdout = stdout

pkg/cmd/extension/manager_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,26 @@ func TestManager_Install(t *testing.T) {
202202
assert.Equal(t, "", stderr.String())
203203
}
204204

205+
func TestManager_Create(t *testing.T) {
206+
tempDir := t.TempDir()
207+
oldWd, _ := os.Getwd()
208+
assert.NoError(t, os.Chdir(tempDir))
209+
t.Cleanup(func() { _ = os.Chdir(oldWd) })
210+
m := newTestManager(tempDir)
211+
err := m.Create("gh-test")
212+
assert.NoError(t, err)
213+
files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test"))
214+
assert.NoError(t, err)
215+
assert.Equal(t, 1, len(files))
216+
extFile := files[0]
217+
assert.Equal(t, "gh-test", extFile.Name())
218+
if runtime.GOOS == "windows" {
219+
assert.Equal(t, os.FileMode(0666), extFile.Mode())
220+
} else {
221+
assert.Equal(t, os.FileMode(0755), extFile.Mode())
222+
}
223+
}
224+
205225
func stubExtension(path string) error {
206226
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
207227
return err

pkg/extensions/extension.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ type ExtensionManager interface {
2121
Upgrade(name string, force bool, stdout, stderr io.Writer) error
2222
Remove(name string) error
2323
Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error)
24+
Create(name string) error
2425
}

pkg/extensions/manager_mock.go

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)