Merge pull request #4159 from cli/ext-create

Add extension create command
This commit is contained in:
Sam 2021-08-23 10:44:54 -07:00 committed by GitHub
commit 25b150ad6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 216 additions and 1 deletions

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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 {