From e9f7459ce2b9086fdb90decbfc5e38f3ff1e0bac Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Thu, 19 Aug 2021 12:08:14 -0700 Subject: [PATCH] Add extension create command --- pkg/cmd/extension/command.go | 33 +++++++++++++++++++++++ pkg/cmd/extension/command_test.go | 45 +++++++++++++++++++++++++++++++ pkg/cmd/extension/manager.go | 27 +++++++++++++++++++ pkg/cmd/extension/manager_test.go | 20 ++++++++++++++ pkg/extensions/extension.go | 1 + pkg/extensions/manager_mock.go | 43 +++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index eaed4618a..89782f96a 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -141,6 +141,39 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command { return nil }, }, + &cobra.Command{ + Use: "create ", + Short: "Create a new extension", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + extName := args[0] + if !strings.HasPrefix(extName, "gh-") { + extName = "gh-" + extName + } + if err := m.Create(extName); err != nil { + return err + } + if io.IsStdoutTTY() { + link := "https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions" + cs := io.ColorScheme() + out := heredoc.Docf(` + %[1]s Created directory %[2]s + %[1]s Initialized git repository + %[1]s Set up extension scaffolding + + %[2]s is ready for development + + Install locally with: cd %[2]s && gh extension install . + + Publish to GitHub with: gh repo create %[2]s + + For more information on writing extensions: %[3]s + `, cs.SuccessIcon(), extName, link) + fmt.Fprint(io.Out, out) + } + return nil + }, + }, ) extCmd.Hidden = true diff --git a/pkg/cmd/extension/command_test.go b/pkg/cmd/extension/command_test.go index 73639fac4..d9668ed0d 100644 --- a/pkg/cmd/extension/command_test.go +++ b/pkg/cmd/extension/command_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/internal/config" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/extensions" @@ -161,6 +162,50 @@ func TestNewCmdExtension(t *testing.T) { }, wantStdout: "gh test\tcli/gh-test\t\ngh test2\tcli/gh-test2\tUpgrade available\n", }, + { + name: "create extension tty", + args: []string{"create", "test"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.CreateFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.CreateCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "gh-test", calls[0].Name) + } + }, + isTTY: true, + wantStdout: heredoc.Doc(` + ✓ Created directory gh-test + ✓ Initialized git repository + ✓ Set up extension scaffolding + + gh-test is ready for development + + Install locally with: cd gh-test && gh extension install . + + Publish to GitHub with: gh repo create gh-test + + For more information on writing extensions: https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions + `), + }, + { + name: "create extension notty", + args: []string{"create", "gh-test"}, + managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) { + em.CreateFunc = func(name string) error { + return nil + } + return func(t *testing.T) { + calls := em.CreateCalls() + assert.Equal(t, 1, len(calls)) + assert.Equal(t, "gh-test", calls[0].Name) + } + }, + isTTY: false, + wantStdout: "", + }, } for _, tt := range tests { diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index e6fb8b751..82717aa95 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -13,6 +13,7 @@ import ( "runtime" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/internal/config" "github.com/cli/cli/pkg/extensions" "github.com/cli/cli/pkg/findsh" @@ -249,6 +250,32 @@ func (m *Manager) installDir() string { return filepath.Join(m.dataDir(), "extensions") } +func (m *Manager) Create(name string) error { + exe, err := m.lookPath("git") + if err != nil { + return err + } + + err = os.Mkdir(name, 0755) + if err != nil { + return err + } + + initCmd := m.newCommand(exe, "init", "-b", "main", "--quiet", name) + err = initCmd.Run() + if err != nil { + return err + } + + fileTmpl := heredoc.Docf(` + #!/bin/bash + echo "Hello %[1]s" + `, name) + filePath := filepath.Join(name, name) + err = ioutil.WriteFile(filePath, []byte(fileTmpl), 0755) + return err +} + func runCmds(cmds []*exec.Cmd, stdout, stderr io.Writer) error { for _, cmd := range cmds { cmd.Stdout = stdout diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index a5e335636..2fd458bf1 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -202,6 +202,26 @@ func TestManager_Install(t *testing.T) { assert.Equal(t, "", stderr.String()) } +func TestManager_Create(t *testing.T) { + tempDir := t.TempDir() + oldWd, _ := os.Getwd() + assert.NoError(t, os.Chdir(tempDir)) + t.Cleanup(func() { _ = os.Chdir(oldWd) }) + m := newTestManager(tempDir) + err := m.Create("gh-test") + assert.NoError(t, err) + files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test")) + assert.NoError(t, err) + assert.Equal(t, 1, len(files)) + extFile := files[0] + assert.Equal(t, "gh-test", extFile.Name()) + if runtime.GOOS == "windows" { + assert.Equal(t, os.FileMode(0666), extFile.Mode()) + } else { + assert.Equal(t, os.FileMode(0755), extFile.Mode()) + } +} + func stubExtension(path string) error { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go index e6e351877..54379ec7f 100644 --- a/pkg/extensions/extension.go +++ b/pkg/extensions/extension.go @@ -21,4 +21,5 @@ type ExtensionManager interface { Upgrade(name string, force bool, stdout, stderr io.Writer) error Remove(name string) error Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) + Create(name string) error } diff --git a/pkg/extensions/manager_mock.go b/pkg/extensions/manager_mock.go index 31b4800cf..157262698 100644 --- a/pkg/extensions/manager_mock.go +++ b/pkg/extensions/manager_mock.go @@ -18,6 +18,9 @@ var _ ExtensionManager = &ExtensionManagerMock{} // // // make and configure a mocked ExtensionManager // mockedExtensionManager := &ExtensionManagerMock{ +// CreateFunc: func(name string) error { +// panic("mock out the Create method") +// }, // DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { // panic("mock out the Dispatch method") // }, @@ -43,6 +46,9 @@ var _ ExtensionManager = &ExtensionManagerMock{} // // } type ExtensionManagerMock struct { + // CreateFunc mocks the Create method. + CreateFunc func(name string) error + // DispatchFunc mocks the Dispatch method. DispatchFunc func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) @@ -63,6 +69,11 @@ type ExtensionManagerMock struct { // calls tracks calls to the methods. calls struct { + // Create holds details about calls to the Create method. + Create []struct { + // Name is the name argument value. + Name string + } // Dispatch holds details about calls to the Dispatch method. Dispatch []struct { // Args is the args argument value. @@ -110,6 +121,7 @@ type ExtensionManagerMock struct { Stderr io.Writer } } + lockCreate sync.RWMutex lockDispatch sync.RWMutex lockInstall sync.RWMutex lockInstallLocal sync.RWMutex @@ -118,6 +130,37 @@ type ExtensionManagerMock struct { lockUpgrade sync.RWMutex } +// Create calls CreateFunc. +func (mock *ExtensionManagerMock) Create(name string) error { + if mock.CreateFunc == nil { + panic("ExtensionManagerMock.CreateFunc: method is nil but ExtensionManager.Create was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockCreate.Lock() + mock.calls.Create = append(mock.calls.Create, callInfo) + mock.lockCreate.Unlock() + return mock.CreateFunc(name) +} + +// CreateCalls gets all the calls that were made to Create. +// Check the length with: +// len(mockedExtensionManager.CreateCalls()) +func (mock *ExtensionManagerMock) CreateCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockCreate.RLock() + calls = mock.calls.Create + mock.lockCreate.RUnlock() + return calls +} + // Dispatch calls DispatchFunc. func (mock *ExtensionManagerMock) Dispatch(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { if mock.DispatchFunc == nil {