Merge pull request #4159 from cli/ext-create
Add extension create command
This commit is contained in:
commit
25b150ad6e
6 changed files with 216 additions and 1 deletions
|
|
@ -141,9 +141,43 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
|
|||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "create <name>",
|
||||
Short: "Create a new extension",
|
||||
Args: cmdutil.ExactArgs(1, "must specify a name for the extension"),
|
||||
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() {
|
||||
return nil
|
||||
}
|
||||
link := "https://docs.github.com/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
|
||||
return &extCmd
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -221,6 +222,51 @@ 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/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 {
|
||||
|
|
|
|||
|
|
@ -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,76 @@ 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", "--quiet", name)
|
||||
err = initCmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileTmpl := heredoc.Docf(`
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Hello %[1]s!"
|
||||
|
||||
# Snippets to help get started:
|
||||
|
||||
# Determine if an executable is in the PATH
|
||||
# if ! type -p ruby >/dev/null; then
|
||||
# echo "Ruby not found on the system" >&2
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# Pass arguments through to another command
|
||||
# gh issue list "$@" -R cli/cli
|
||||
|
||||
# Using the gh api command to retrieve and format information
|
||||
# QUERY='
|
||||
# query($endCursor: String) {
|
||||
# viewer {
|
||||
# repositories(first: 100, after: $endCursor) {
|
||||
# nodes {
|
||||
# nameWithOwner
|
||||
# stargazerCount
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# '
|
||||
# TEMPLATE='
|
||||
# {{- range $repo := .data.viewer.repositories.nodes -}}
|
||||
# {{- printf "name: %[2]s - stargazers: %[3]s\n" $repo.nameWithOwner $repo.stargazerCount -}}
|
||||
# {{- end -}}
|
||||
# '
|
||||
# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}"
|
||||
`, name, "%s", "%v")
|
||||
filePath := filepath.Join(name, name)
|
||||
err = ioutil.WriteFile(filePath, []byte(fileTmpl), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Join(wd, name)
|
||||
addCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", name, "--chmod=+x")
|
||||
err = addCmd.Run()
|
||||
return err
|
||||
}
|
||||
|
||||
func runCmds(cmds []*exec.Cmd, stdout, stderr io.Writer) error {
|
||||
for _, cmd := range cmds {
|
||||
cmd.Stdout = stdout
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue