diff --git a/pkg/cmd/extension/ext_tmpls/buildScript.sh b/pkg/cmd/extension/ext_tmpls/buildScript.sh index 744667533..df82ded26 100644 --- a/pkg/cmd/extension/ext_tmpls/buildScript.sh +++ b/pkg/cmd/extension/ext_tmpls/buildScript.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash echo "TODO implement this script." echo "It should build binaries in dist/-[.exe] as needed." -exit 1 \ No newline at end of file +exit 1 diff --git a/pkg/cmd/extension/ext_tmpls/goBinMain.go.txt b/pkg/cmd/extension/ext_tmpls/goBinMain.go.txt index 9e29dee8b..c9d2bdd43 100644 --- a/pkg/cmd/extension/ext_tmpls/goBinMain.go.txt +++ b/pkg/cmd/extension/ext_tmpls/goBinMain.go.txt @@ -23,4 +23,4 @@ func main() { } // For more examples of using go-gh, see: -// https://github.com/cli/go-gh/blob/trunk/example_gh_test.go \ No newline at end of file +// https://github.com/cli/go-gh/blob/trunk/example_gh_test.go diff --git a/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml b/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml index 294290de0..0266208e0 100644 --- a/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml +++ b/pkg/cmd/extension/ext_tmpls/goBinWorkflow.yml @@ -11,4 +11,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: cli/gh-extension-precompile@v1 \ No newline at end of file + - uses: cli/gh-extension-precompile@v1 diff --git a/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml b/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml index 08f077695..ac67c3c78 100644 --- a/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml +++ b/pkg/cmd/extension/ext_tmpls/otherBinWorkflow.yml @@ -13,4 +13,4 @@ jobs: - uses: actions/checkout@v2 - uses: cli/gh-extension-precompile@v1 with: - build_script_override: "script/build.sh" \ No newline at end of file + build_script_override: "script/build.sh" diff --git a/pkg/cmd/extension/ext_tmpls/script.sh b/pkg/cmd/extension/ext_tmpls/script.sh index 198ec0ee8..75e3dee14 100644 --- a/pkg/cmd/extension/ext_tmpls/script.sh +++ b/pkg/cmd/extension/ext_tmpls/script.sh @@ -29,7 +29,7 @@ echo "Hello %[1]s!" # ' # TEMPLATE=' # {{- range $repo := .data.viewer.repositories.nodes -}} -# {{- printf "name: %[2]s - stargazers: %[3]s\n" $repo.nameWithOwner $repo.stargazerCount -}} +# {{- printf "name: %%s - stargazers: %%v\n" $repo.nameWithOwner $repo.stargazerCount -}} # {{- end -}} # ' -# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}" \ No newline at end of file +# exec gh api graphql -f query="${QUERY}" --paginate --template="${TEMPLATE}" diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 8ef2f0ff6..a90d59e28 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -510,17 +510,14 @@ func (m *Manager) upgradeGitExtension(ext Extension, force bool) error { if err != nil { return err } - var cmds []*exec.Cmd dir := filepath.Dir(ext.path) if force { - fetchCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "fetch", "origin", "HEAD") - resetCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "reset", "--hard", "origin/HEAD") - cmds = []*exec.Cmd{fetchCmd, resetCmd} - } else { - pullCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only") - cmds = []*exec.Cmd{pullCmd} + if err := m.newCommand(exe, "-C", dir, "fetch", "origin", "HEAD").Run(); err != nil { + return err + } + return m.newCommand(exe, "-C", dir, "reset", "--hard", "origin/HEAD").Run() } - return runCmds(cmds) + return m.newCommand(exe, "-C", dir, "pull", "--ff-only").Run() } func (m *Manager) upgradeBinExtension(ext Extension) error { @@ -564,14 +561,7 @@ func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error 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 { + if err := m.newCommand(exe, "init", "--quiet", name).Run(); err != nil { return err } @@ -581,55 +571,26 @@ func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error return m.otherBinScaffolding(exe, name) } - script := fmt.Sprintf(scriptTmpl, name, "%s", "%v") - filePath := filepath.Join(name, name) - err = ioutil.WriteFile(filePath, []byte(script), 0755) - if err != nil { + script := fmt.Sprintf(scriptTmpl, name) + if err := writeFile(filepath.Join(name, name), []byte(script), 0755); 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") - return addCmd.Run() + return m.newCommand(exe, "-C", name, "add", name, "--chmod=+x").Run() } func (m *Manager) otherBinScaffolding(gitExe, name string) error { - err := os.MkdirAll(filepath.Join(name, ".github", "workflows"), 0755) - if err != nil { - return err - } - workflowPath := filepath.Join(".github", "workflows", "release.yml") - err = ioutil.WriteFile(filepath.Join(name, workflowPath), otherBinWorkflow, 0644) - if err != nil { - return err - } - err = os.Mkdir(filepath.Join(name, "script"), 0755) - if err != nil { + if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), otherBinWorkflow, 0644); err != nil { return err } buildScriptPath := filepath.Join("script", "build.sh") - err = ioutil.WriteFile(filepath.Join(name, buildScriptPath), buildScript, 0755) - if err != nil { + if err := writeFile(filepath.Join(name, buildScriptPath), buildScript, 0755); err != nil { return err } - - wd, err := os.Getwd() - if err != nil { + if err := m.newCommand(gitExe, "-C", name, "add", buildScriptPath, "--chmod=+x").Run(); err != nil { return err } - dir := filepath.Join(wd, name) - addCmd := m.newCommand(gitExe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", buildScriptPath, "--chmod=+x") - err = addCmd.Run() - if err != nil { - return err - } - - addCmd = m.newCommand(gitExe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "add", workflowPath) - return addCmd.Run() + return m.newCommand(gitExe, "-C", name, "add", ".").Run() } func (m *Manager) goBinScaffolding(gitExe, name string) error { @@ -637,30 +598,16 @@ func (m *Manager) goBinScaffolding(gitExe, name string) error { if err != nil { return fmt.Errorf("go is required for creating Go extensions: %w", err) } - err = os.MkdirAll(filepath.Join(name, ".github", "workflows"), 0755) - if err != nil { - return err - } - workflowPath := filepath.Join(".github", "workflows", "release.yml") - err = ioutil.WriteFile(filepath.Join(name, workflowPath), goBinWorkflow, 0644) - if err != nil { + + if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), goBinWorkflow, 0644); err != nil { return err } mainGo := fmt.Sprintf(mainGoTmpl, name) - mainPath := "main.go" - - err = ioutil.WriteFile(filepath.Join(name, mainPath), []byte(mainGo), 0644) - if err != nil { + if err := writeFile(filepath.Join(name, "main.go"), []byte(mainGo), 0644); err != nil { return err } - wd, err := os.Getwd() - if err != nil { - return err - } - dir := filepath.Join(wd, name) - host, err := m.config.DefaultHost() if err != nil { return err @@ -677,51 +624,35 @@ func (m *Manager) goBinScaffolding(gitExe, name string) error { {"build"}, } - _, ext := m.platform() - ignore := name + ext - - err = ioutil.WriteFile(filepath.Join(name, ".gitignore"), []byte(ignore), 0644) - if err != nil { + ignore := fmt.Sprintf("/%[1]s\n/%[1]s.exe\n", name) + if err := writeFile(filepath.Join(name, ".gitignore"), []byte(ignore), 0644); err != nil { return err } for _, args := range goCmds { goCmd := m.newCommand(goExe, args...) - goCmd.Dir = dir - err = goCmd.Run() - if err != nil { + goCmd.Dir = name + if err := goCmd.Run(); err != nil { return fmt.Errorf("failed to set up go module: %w", err) } } - addArgs := []string{ - "-C", dir, - "--git-dir=" + filepath.Join(dir, ".git"), - "add", - workflowPath, - mainPath, - ".gitignore", - "go.mod", - "go.sum", - } - - addCmd := m.newCommand(gitExe, addArgs...) - return addCmd.Run() -} - -func runCmds(cmds []*exec.Cmd) error { - for _, cmd := range cmds { - if err := cmd.Run(); err != nil { - return err - } - } - return nil + return m.newCommand(gitExe, "-C", name, "add", ".").Run() } func isSymlink(m os.FileMode) bool { return m&os.ModeSymlink != 0 } +func writeFile(p string, contents []byte, mode os.FileMode) error { + if dir := filepath.Dir(p); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + return os.WriteFile(p, contents, mode) +} + // reads the product of makeSymlink on Windows func readPathFromFile(path string) (string, error) { f, err := os.Open(path) diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 6320195a1..47f11d348 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -9,6 +9,8 @@ import ( "os/exec" "path/filepath" "runtime" + "sort" + "strings" "testing" "github.com/MakeNowJust/heredoc" @@ -19,6 +21,7 @@ import ( "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -27,6 +30,15 @@ func TestHelperProcess(t *testing.T) { return } if err := func(args []string) error { + // git init should create the directory named by argument + if len(args) > 2 && strings.HasPrefix(strings.Join(args, " "), "git init") { + dir := args[len(args)-1] + if !strings.HasPrefix(dir, "-") { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + } fmt.Fprintf(os.Stdout, "%v\n", args) return nil }(os.Args[3:]); err != nil { @@ -219,16 +231,14 @@ func TestManager_UpgradeExtensions(t *testing.T) { assert.NoError(t, err) assert.Equal(t, heredoc.Docf( ` - [hello]: [git -C %s --git-dir=%s pull --ff-only] + [hello]: [git -C %s pull --ff-only] upgrade complete [local]: local extensions can not be upgraded - [two]: [git -C %s --git-dir=%s pull --ff-only] + [two]: [git -C %s pull --ff-only] upgrade complete `, filepath.Join(tempDir, "extensions", "gh-hello"), - filepath.Join(tempDir, "extensions", "gh-hello", ".git"), filepath.Join(tempDir, "extensions", "gh-two"), - filepath.Join(tempDir, "extensions", "gh-two", ".git"), ), stdout.String()) assert.Equal(t, "", stderr.String()) } @@ -263,10 +273,9 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) { assert.NoError(t, err) assert.Equal(t, heredoc.Docf( ` - [git -C %s --git-dir=%s pull --ff-only] + [git -C %s pull --ff-only] `, filepath.Join(tempDir, "extensions", "gh-remote"), - filepath.Join(tempDir, "extensions", "gh-remote", ".git"), ), stdout.String()) assert.Equal(t, "", stderr.String()) } @@ -274,7 +283,6 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) { func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) { tempDir := t.TempDir() extensionDir := filepath.Join(tempDir, "extensions", "gh-remote") - gitDir := filepath.Join(tempDir, "extensions", "gh-remote", ".git") assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) io, _, stdout, stderr := iostreams.Test() m := newTestManager(tempDir, nil, io) @@ -288,13 +296,10 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) { assert.NoError(t, err) assert.Equal(t, heredoc.Docf( ` - [git -C %s --git-dir=%s fetch origin HEAD] - [git -C %s --git-dir=%s reset --hard origin/HEAD] + [git -C %[1]s fetch origin HEAD] + [git -C %[1]s reset --hard origin/HEAD] `, extensionDir, - gitDir, - extensionDir, - gitDir, ), stdout.String()) assert.Equal(t, "", stderr.String()) } @@ -378,10 +383,10 @@ func TestManager_MigrateToBinaryExtension(t *testing.T) { func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { tempDir := t.TempDir() - io, _, _, _ := iostreams.Test() + reg := httpmock.Registry{} defer reg.Verify(t) - client := http.Client{Transport: ®} + assert.NoError(t, stubBinaryExtension( filepath.Join(tempDir, "extensions", "gh-bin-ext"), binManifest{ @@ -390,7 +395,9 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { Host: "example.com", Tag: "v1.0.1", })) - m := newTestManager(tempDir, &client, io) + + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(tempDir, &http.Client{Transport: ®}, io) reg.Register( httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), httpmock.JSONResponse( @@ -432,8 +439,10 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) { fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) assert.NoError(t, err) - assert.Equal(t, "FAKE UPGRADED BINARY", string(fakeBin)) + + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) } func TestManager_Install_git(t *testing.T) { @@ -444,7 +453,6 @@ func TestManager_Install_git(t *testing.T) { client := http.Client{Transport: ®} io, _, stdout, stderr := iostreams.Test() - m := newTestManager(tempDir, &client, io) reg.Register( @@ -501,17 +509,16 @@ func TestManager_Install_binary_unsupported(t *testing.T) { }, })) - io, _, _, _ := iostreams.Test() + io, _, stdout, stderr := iostreams.Test() tempDir := t.TempDir() m := newTestManager(tempDir, &client, io) err := m.Install(repo) - assert.Error(t, err) + assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`") - errText := "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`" - - assert.Equal(t, errText, err.Error()) + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) } func TestManager_Install_binary(t *testing.T) { @@ -519,7 +526,6 @@ func TestManager_Install_binary(t *testing.T) { reg := httpmock.Registry{} defer reg.Verify(t) - client := http.Client{Transport: ®} reg.Register( httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), @@ -548,10 +554,10 @@ func TestManager_Install_binary(t *testing.T) { httpmock.REST("GET", "release/cool"), httpmock.StringResponse("FAKE BINARY")) - io, _, _, _ := iostreams.Test() + io, _, stdout, stderr := iostreams.Test() tempDir := t.TempDir() - m := newTestManager(tempDir, &client, io) + m := newTestManager(tempDir, &http.Client{Transport: ®}, io) err := m.Install(repo) assert.NoError(t, err) @@ -573,85 +579,116 @@ func TestManager_Install_binary(t *testing.T) { fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext.exe")) assert.NoError(t, err) - assert.Equal(t, "FAKE BINARY", string(fakeBin)) + + assert.Equal(t, "", stdout.String()) + 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, nil, nil) + chdirTemp(t) + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(".", nil, io) + err := m.Create("gh-test", extensions.GitTemplateType) assert.NoError(t, err) - files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test")) + files, err := ioutil.ReadDir("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()) - } + assert.Equal(t, []string{"gh-test"}, fileNames(files)) + + assert.Equal(t, heredoc.Doc(` + [git init --quiet gh-test] + [git -C gh-test add gh-test --chmod=+x] + `), stdout.String()) + assert.Equal(t, "", stderr.String()) } func TestManager_Create_go_binary(t *testing.T) { - tempDir := t.TempDir() - oldWd, _ := os.Getwd() - assert.NoError(t, os.Chdir(tempDir)) - t.Cleanup(func() { _ = os.Chdir(oldWd) }) + chdirTemp(t) reg := httpmock.Registry{} defer reg.Verify(t) - client := http.Client{Transport: ®} - reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) - m := newTestManager(tempDir, &client, nil) + + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(".", &http.Client{Transport: ®}, io) err := m.Create("gh-test", extensions.GoBinTemplateType) - assert.NoError(t, err) + require.NoError(t, err) - files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test")) - assert.NoError(t, err) - assert.Equal(t, 3, len(files)) - assert.Equal(t, ".gitignore", files[1].Name()) - assert.Equal(t, "main.go", files[2].Name()) + files, err := ioutil.ReadDir("gh-test") + require.NoError(t, err) + assert.Equal(t, []string{".github", ".gitignore", "main.go"}, fileNames(files)) - files, err = ioutil.ReadDir(filepath.Join(tempDir, "gh-test", ".github", "workflows")) - assert.NoError(t, err) - assert.Equal(t, 1, len(files)) - workflowFile := files[0] - assert.Equal(t, "release.yml", workflowFile.Name()) + gitignore, err := os.ReadFile(filepath.Join("gh-test", ".gitignore")) + require.NoError(t, err) + assert.Equal(t, heredoc.Doc(` + /gh-test + /gh-test.exe + `), string(gitignore)) + + files, err = ioutil.ReadDir(filepath.Join("gh-test", ".github", "workflows")) + require.NoError(t, err) + assert.Equal(t, []string{"release.yml"}, fileNames(files)) + + assert.Equal(t, heredoc.Doc(` + [git init --quiet gh-test] + [go mod init github.com/jillv/gh-test] + [go mod tidy] + [go build] + [git -C gh-test add .] + `), stdout.String()) + assert.Equal(t, "", stderr.String()) } func TestManager_Create_other_binary(t *testing.T) { - tempDir := t.TempDir() - oldWd, _ := os.Getwd() - assert.NoError(t, os.Chdir(tempDir)) - t.Cleanup(func() { _ = os.Chdir(oldWd) }) - m := newTestManager(tempDir, nil, nil) + chdirTemp(t) + io, _, stdout, stderr := iostreams.Test() + m := newTestManager(".", nil, io) err := m.Create("gh-test", extensions.OtherBinTemplateType) assert.NoError(t, err) - files, err := ioutil.ReadDir(filepath.Join(tempDir, "gh-test")) + files, err := ioutil.ReadDir("gh-test") assert.NoError(t, err) assert.Equal(t, 2, len(files)) - files, err = ioutil.ReadDir(filepath.Join(tempDir, "gh-test", ".github", "workflows")) + files, err = ioutil.ReadDir(filepath.Join("gh-test", ".github", "workflows")) assert.NoError(t, err) - assert.Equal(t, 1, len(files)) - workflowFile := files[0] - assert.Equal(t, "release.yml", workflowFile.Name()) + assert.Equal(t, []string{"release.yml"}, fileNames(files)) - files, err = ioutil.ReadDir(filepath.Join(tempDir, "gh-test", "script")) + files, err = ioutil.ReadDir(filepath.Join("gh-test", "script")) assert.NoError(t, err) - assert.Equal(t, 1, len(files)) - buildFile := files[0] - assert.Equal(t, "build.sh", buildFile.Name()) + assert.Equal(t, []string{"build.sh"}, fileNames(files)) + + assert.Equal(t, heredoc.Docf(` + [git init --quiet gh-test] + [git -C gh-test add %s --chmod=+x] + [git -C gh-test add .] + `, filepath.FromSlash("script/build.sh")), stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +// chdirTemp changes the current working directory to a temporary directory for the duration of the test. +func chdirTemp(t *testing.T) { + oldWd, _ := os.Getwd() + tempDir := t.TempDir() + if err := os.Chdir(tempDir); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = os.Chdir(oldWd) + }) +} + +func fileNames(files []os.FileInfo) []string { + names := make([]string, len(files)) + for i, f := range files { + names[i] = f.Name() + } + sort.Strings(names) + return names } func stubExtension(path string) error {