diff --git a/execute/testdata/script b/execute/testdata/script new file mode 100755 index 00000000..7e89bf37 --- /dev/null +++ b/execute/testdata/script @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo $@ diff --git a/git/git_test.go b/git/git_test.go index c73bd89f..dfd4b945 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -5,35 +5,27 @@ import ( "path/filepath" "testing" + "asdf/repotest" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/stretchr/testify/assert" ) -// TODO: Switch to local repo so tests don't go over the network -const ( - testRepo = "https://github.com/Stratus3D/asdf-lua" - testPluginName = "lua" -) - func TestPluginClone(t *testing.T) { t.Run("when plugin name is valid but URL is invalid prints an error", func(t *testing.T) { - tempDir := t.TempDir() - directory := filepath.Join(tempDir, testPluginName) - - plugin := NewRepo(directory) + plugin := NewRepo(t.TempDir()) err := plugin.Clone("foobar") assert.ErrorContains(t, err, "unable to clone plugin: repository not found") }) t.Run("clones provided Git URL to plugin directory when URL is valid", func(t *testing.T) { - tempDir := t.TempDir() - directory := filepath.Join(tempDir, testPluginName) - + repoDir := generateRepo(t) + directory := t.TempDir() plugin := NewRepo(directory) - err := plugin.Clone(testRepo) + err := plugin.Clone(repoDir) assert.Nil(t, err) // Assert plugin directory contains Git repo with bin directory @@ -42,17 +34,17 @@ func TestPluginClone(t *testing.T) { entries, err := os.ReadDir(directory + "/bin") assert.Nil(t, err) - assert.Equal(t, 5, len(entries)) + assert.Equal(t, 12, len(entries)) }) } func TestPluginHead(t *testing.T) { - tempDir := t.TempDir() - directory := filepath.Join(tempDir, testPluginName) + repoDir := generateRepo(t) + directory := t.TempDir() plugin := NewRepo(directory) - err := plugin.Clone(testRepo) + err := plugin.Clone(repoDir) assert.Nil(t, err) head, err := plugin.Head() @@ -61,12 +53,12 @@ func TestPluginHead(t *testing.T) { } func TestPluginRemoteURL(t *testing.T) { - tempDir := t.TempDir() - directory := filepath.Join(tempDir, testPluginName) + repoDir := generateRepo(t) + directory := t.TempDir() plugin := NewRepo(directory) - err := plugin.Clone(testRepo) + err := plugin.Clone(repoDir) assert.Nil(t, err) url, err := plugin.RemoteURL() @@ -75,16 +67,16 @@ func TestPluginRemoteURL(t *testing.T) { } func TestPluginUpdate(t *testing.T) { - tempDir := t.TempDir() - directory := filepath.Join(tempDir, testPluginName) + repoDir := generateRepo(t) + directory := t.TempDir() plugin := NewRepo(directory) - err := plugin.Clone(testRepo) + err := plugin.Clone(repoDir) assert.Nil(t, err) t.Run("returns error when plugin with name does not exist", func(t *testing.T) { - nonexistantPath := filepath.Join(tempDir, "nonexistant") + nonexistantPath := filepath.Join(directory, "nonexistant") nonexistantPlugin := NewRepo(nonexistantPath) updatedToRef, err := nonexistantPlugin.Update("") @@ -96,7 +88,7 @@ func TestPluginUpdate(t *testing.T) { t.Run("returns error when plugin repo does not exist", func(t *testing.T) { badPluginName := "badplugin" - badPluginDir := filepath.Join(tempDir, badPluginName) + badPluginDir := filepath.Join(directory, badPluginName) err := os.MkdirAll(badPluginDir, 0o777) assert.Nil(t, err) @@ -198,3 +190,12 @@ func checkoutPreviousCommit(path string) (string, error) { return previousHash.String(), nil } + +func generateRepo(t *testing.T) string { + t.Helper() + tempDir := t.TempDir() + path, err := repotest.GeneratePlugin("dummy_plugin", tempDir, "lua") + + assert.Nil(t, err) + return path +} diff --git a/go.mod b/go.mod index a02e302b..65d0215e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.5 require ( github.com/go-git/go-git/v5 v5.11.0 github.com/mitchellh/go-homedir v1.1.0 + github.com/otiai10/copy v1.14.0 github.com/sethvargo/go-envconfig v1.0.0 github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.27.1 @@ -35,6 +36,7 @@ require ( golang.org/x/crypto v0.16.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/tools v0.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index f4f5cf7e..7e759c71 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,10 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/pluginindex/pluginindex.go b/pluginindex/pluginindex.go index 5859bbea..d5d4c677 100644 --- a/pluginindex/pluginindex.go +++ b/pluginindex/pluginindex.go @@ -63,7 +63,7 @@ func (p PluginIndex) Refresh() (bool, error) { // directory empty, clone down repo err := p.repo.Clone(p.url) if err != nil { - return false, err + return false, fmt.Errorf("unable to initialize index: %w", err) } return touchFS(p.directory) @@ -90,7 +90,7 @@ func (p PluginIndex) doUpdate() (bool, error) { // commit is _, err := p.repo.Update("") if err != nil { - return false, err + return false, fmt.Errorf("unable to update plugin index: %w", err) } // Touch update file diff --git a/pluginindex/pluginindex_test.go b/pluginindex/pluginindex_test.go index 5768f5e6..efb2b785 100644 --- a/pluginindex/pluginindex_test.go +++ b/pluginindex/pluginindex_test.go @@ -9,13 +9,15 @@ import ( "time" "asdf/git" + "asdf/repotest" "github.com/stretchr/testify/assert" ) const ( - realIndexURL = "https://github.com/asdf-vm/asdf-plugins.git" + mockIndexURL = "https://github.com/asdf-vm/asdf-plugins.git" badIndexURL = "http://asdf-vm.com/non-existent" + fooPluginURL = "http://example.com/foo" elixirPluginURL = "https://github.com/asdf-vm/asdf-elixir.git" erlangPluginURL = "https://github.com/asdf-vm/asdf-erlang.git" ) @@ -84,15 +86,22 @@ func writeMockPluginFile(dir, pluginName, pluginURL string) error { func TestGetPluginSourceURL(t *testing.T) { t.Run("with Git returns a plugin url when provided name of existing plugin", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, true, 0, &git.Repo{Directory: dir}) - url, err := pluginIndex.GetPluginSourceURL("elixir") + indexDir := filepath.Join(dir, "index") + err := os.Mkdir(indexDir, 0o777) assert.Nil(t, err) - assert.Equal(t, url, elixirPluginURL) + + repoPath, err := repotest.GeneratePluginIndex(dir) + assert.Nil(t, err) + + pluginIndex := New(indexDir, repoPath, true, 0, &git.Repo{Directory: indexDir}) + url, err := pluginIndex.GetPluginSourceURL("foo") + assert.Nil(t, err) + assert.Equal(t, url, fooPluginURL) }) t.Run("returns a plugin url when provided name of existing plugin", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, true, 0, &MockIndex{Directory: dir}) + pluginIndex := New(dir, mockIndexURL, true, 0, &MockIndex{Directory: dir}) url, err := pluginIndex.GetPluginSourceURL("elixir") assert.Nil(t, err) assert.Equal(t, url, elixirPluginURL) @@ -100,7 +109,7 @@ func TestGetPluginSourceURL(t *testing.T) { t.Run("returns a plugin url when provided name of existing plugin when loading from cache", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, false, 10, &MockIndex{Directory: dir}) + pluginIndex := New(dir, mockIndexURL, false, 10, &MockIndex{Directory: dir}) url, err := pluginIndex.GetPluginSourceURL("elixir") assert.Nil(t, err) assert.Equal(t, url, elixirPluginURL) @@ -112,7 +121,7 @@ func TestGetPluginSourceURL(t *testing.T) { t.Run("returns an error when given a name that isn't in the index", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, false, 10, &MockIndex{Directory: dir}) + pluginIndex := New(dir, mockIndexURL, false, 10, &MockIndex{Directory: dir}) url, err := pluginIndex.GetPluginSourceURL("foobar") assert.EqualError(t, err, "plugin foobar not found in repository") assert.Equal(t, url, "") @@ -130,7 +139,7 @@ func TestGetPluginSourceURL(t *testing.T) { pluginIndex := New(dir, badIndexURL, false, 10, &repo) url, err := pluginIndex.GetPluginSourceURL("lua") - assert.EqualError(t, err, "unable to clone: repository not found") + assert.EqualError(t, err, "unable to update plugin index: unable to clone: repository not found") assert.Equal(t, url, "") }) @@ -138,7 +147,7 @@ func TestGetPluginSourceURL(t *testing.T) { dir := t.TempDir() pluginIndex := New(dir, badIndexURL, false, 10, &MockIndex{Directory: dir}) url, err := pluginIndex.GetPluginSourceURL("lua") - assert.EqualError(t, err, "unable to clone: repository not found") + assert.EqualError(t, err, "unable to initialize index: unable to clone: repository not found") assert.Equal(t, url, "") }) } @@ -146,10 +155,17 @@ func TestGetPluginSourceURL(t *testing.T) { func TestRefresh(t *testing.T) { t.Run("with Git updates repo when called once", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, false, 0, &git.Repo{Directory: dir}) - url, err := pluginIndex.GetPluginSourceURL("elixir") + indexDir := filepath.Join(dir, "index") + err := os.Mkdir(indexDir, 0o777) assert.Nil(t, err) - assert.Equal(t, url, elixirPluginURL) + + repoPath, err := repotest.GeneratePluginIndex(dir) + assert.Nil(t, err) + + pluginIndex := New(indexDir, repoPath, false, 0, &git.Repo{Directory: indexDir}) + url, err := pluginIndex.GetPluginSourceURL("foo") + assert.Nil(t, err) + assert.Equal(t, url, fooPluginURL) updated, err := pluginIndex.Refresh() assert.Nil(t, err) @@ -158,7 +174,7 @@ func TestRefresh(t *testing.T) { t.Run("updates repo when called once", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, false, 0, &MockIndex{Directory: dir}) + pluginIndex := New(dir, mockIndexURL, false, 0, &MockIndex{Directory: dir}) updated, err := pluginIndex.Refresh() assert.Nil(t, err) @@ -171,7 +187,7 @@ func TestRefresh(t *testing.T) { t.Run("does not update index when time has not elaspsed", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, false, 10, &MockIndex{Directory: dir}) + pluginIndex := New(dir, mockIndexURL, false, 10, &MockIndex{Directory: dir}) // Call Refresh twice, the second call should not perform an update updated, err := pluginIndex.Refresh() @@ -185,7 +201,7 @@ func TestRefresh(t *testing.T) { t.Run("updates plugin index when time has elaspsed", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, realIndexURL, false, 0, &MockIndex{Directory: dir}) + pluginIndex := New(dir, mockIndexURL, false, 0, &MockIndex{Directory: dir}) // Call Refresh twice, the second call should perform an update updated, err := pluginIndex.Refresh() @@ -200,10 +216,10 @@ func TestRefresh(t *testing.T) { t.Run("returns error when plugin index repo doesn't exist", func(t *testing.T) { dir := t.TempDir() - pluginIndex := New(dir, badIndexURL, false, 0, &MockIndex{Directory: dir}) + pluginIndex := New(dir, badIndexURL, false, 0, &MockIndex{Directory: dir}) updated, err := pluginIndex.Refresh() - assert.EqualError(t, err, "unable to clone: repository not found") + assert.EqualError(t, err, "unable to initialize index: unable to clone: repository not found") assert.False(t, updated) }) } diff --git a/plugins/plugins_test.go b/plugins/plugins_test.go index e3d26afc..13de2f8d 100644 --- a/plugins/plugins_test.go +++ b/plugins/plugins_test.go @@ -1,28 +1,23 @@ package plugins import ( - "fmt" "os" - "os/exec" "path/filepath" "strings" "testing" "asdf/config" + "asdf/repotest" "github.com/stretchr/testify/assert" ) -// TODO: Switch to local repo so tests don't go over the network -const ( - testRepo = "https://github.com/Stratus3D/asdf-lua" - testPluginName = "lua" -) +const testPluginName = "lua" func TestList(t *testing.T) { testDataDir := t.TempDir() conf := config.Config{DataDir: testDataDir} - testRepo, err := installMockPluginRepo(testDataDir, testPluginName) + testRepo, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName) assert.Nil(t, err) err = Add(conf, testPluginName, testRepo) @@ -93,7 +88,7 @@ func TestAdd(t *testing.T) { for _, invalid := range invalids { t.Run(invalid, func(t *testing.T) { - err := Add(config.Config{}, invalid, testRepo) + err := Add(config.Config{}, invalid, "never-cloned") expectedErrMsg := "is invalid. Name may only contain lowercase letters, numbers, '_', and '-'" if !strings.Contains(err.Error(), expectedErrMsg) { @@ -107,13 +102,16 @@ func TestAdd(t *testing.T) { conf := config.Config{DataDir: testDataDir} // Add plugin - err := Add(conf, testPluginName, testRepo) + repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName) + assert.Nil(t, err) + + err = Add(conf, testPluginName, repoPath) if err != nil { t.Fatal("Expected to be able to add plugin") } // Add it again to trigger error - err = Add(conf, testPluginName, testRepo) + err = Add(conf, testPluginName, repoPath) if err == nil { t.Fatal("expected error got nil") @@ -136,8 +134,10 @@ func TestAdd(t *testing.T) { t.Run("when plugin name and URL are valid installs plugin", func(t *testing.T) { testDataDir := t.TempDir() conf := config.Config{DataDir: testDataDir} + pluginPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName) + assert.Nil(t, err) - err := Add(conf, testPluginName, testRepo) + err = Add(conf, testPluginName, pluginPath) assert.Nil(t, err, "Expected to be able to add plugin") @@ -149,14 +149,17 @@ func TestAdd(t *testing.T) { entries, err := os.ReadDir(pluginDir + "/bin") assert.Nil(t, err) - assert.Equal(t, 5, len(entries)) + assert.Equal(t, 12, len(entries)) }) t.Run("when parameters are valid creates plugin download dir", func(t *testing.T) { testDataDir := t.TempDir() conf := config.Config{DataDir: testDataDir} - err := Add(conf, testPluginName, testRepo) + repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName) + assert.Nil(t, err) + + err = Add(conf, testPluginName, repoPath) assert.Nil(t, err) // Assert download dir exists @@ -170,7 +173,10 @@ func TestRemove(t *testing.T) { testDataDir := t.TempDir() conf := config.Config{DataDir: testDataDir} - err := Add(conf, testPluginName, testRepo) + repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName) + assert.Nil(t, err) + + err = Add(conf, testPluginName, repoPath) assert.Nil(t, err) t.Run("returns error when plugin with name does not exist", func(t *testing.T) { @@ -197,7 +203,7 @@ func TestRemove(t *testing.T) { }) t.Run("removes plugin download dir when passed name of installed plugin", func(t *testing.T) { - err := Add(conf, testPluginName, testRepo) + err := Add(conf, testPluginName, repoPath) assert.Nil(t, err) err = Remove(conf, testPluginName) @@ -214,7 +220,10 @@ func TestUpdate(t *testing.T) { testDataDir := t.TempDir() conf := config.Config{DataDir: testDataDir} - err := Add(conf, testPluginName, testRepo) + repoPath, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName) + assert.Nil(t, err) + + err = Add(conf, testPluginName, repoPath) assert.Nil(t, err) badPluginName := "badplugin" @@ -359,7 +368,7 @@ func TestRunCallback(t *testing.T) { testDataDir := t.TempDir() conf := config.Config{DataDir: testDataDir} - testRepo, err := installMockPluginRepo(testDataDir, testPluginName) + testRepo, err := repotest.GeneratePlugin("dummy_plugin", testDataDir, testPluginName) assert.Nil(t, err) err = Add(conf, testPluginName, testRepo) @@ -412,125 +421,3 @@ func touchFile(name string) error { } return file.Close() } - -func installMockPluginRepo(dataDir, name string) (string, error) { - // Because the legacy dummy plugin directory is relative to the root of this - // project I cannot use the usual testing functions to locate it. To - // determine the location of it we compute the module root, which also - // happens to be the root of the repo. - modRootDir, err := moduleRoot() - if err != nil { - return "", err - } - - location := dataDir + "/repo-" + name - - // Then we specify the path to the dummy plugin relative to the module root - err = runCmd("cp", "-r", filepath.Join(modRootDir, "test/fixtures/dummy_plugin"), location) - if err != nil { - return location, err - } - - // Definitely some opportunities to refactor here. This code might be - // simplified by switching to the Go git library - err = runCmd("git", "-C", location, "init", "-q") - if err != nil { - return location, err - } - - err = runCmd("git", "-C", location, "config", "user.name", "\"Test\"") - if err != nil { - return location, err - } - - err = runCmd("git", "-C", location, "config", "user.email", "\"test@example.com\"") - if err != nil { - return location, err - } - - err = runCmd("git", "-C", location, "add", "-A") - if err != nil { - return location, err - } - - err = runCmd("git", "-C", location, "commit", "-q", "-m", fmt.Sprintf("\"asdf %s plugin init\"", name)) - if err != nil { - return location, err - } - - err = runCmd("touch", filepath.Join(location, "README.md")) - if err != nil { - return location, err - } - - err = runCmd("git", "-C", location, "add", "-A") - if err != nil { - return location, err - } - - err = runCmd("git", "-C", location, "commit", "-q", "-m", fmt.Sprintf("\"asdf %s plugin readme \"", name)) - if err != nil { - return location, err - } - - // kind of ugly but I want a remote with a valid path so I use the same - // location as the remote. Probably should refactor - err = runCmd("git", "-C", location, "remote", "add", "origin", location) - if err != nil { - return location, err - } - - return location, err -} - -func moduleRoot() (string, error) { - currentDir, err := os.Getwd() - if err != nil { - return "", err - } - - return findModuleRoot(currentDir), nil -} - -// Taken from https://github.com/golang/go/blob/9e3b1d53a012e98cfd02de2de8b1bd53522464d4/src/cmd/go/internal/modload/init.go#L1504C1-L1522C2 because that function is in an internal module -// and I can't rely on it. -func findModuleRoot(dir string) (roots string) { - if dir == "" { - panic("dir not set") - } - dir = filepath.Clean(dir) - - // Look for enclosing go.mod. - for { - if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() { - return dir - } - d := filepath.Dir(dir) - if d == dir { - break - } - dir = d - } - return "" -} - -// helper function to make running commands easier -func runCmd(cmdName string, args ...string) error { - cmd := exec.Command(cmdName, args...) - - // Capture stdout and stderr - var stdout strings.Builder - var stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - if err != nil { - // If command fails print both stderr and stdout - fmt.Println("stdout:", stdout.String()) - fmt.Println("stderr:", stderr.String()) - return err - } - - return nil -} diff --git a/repotest/repotest.go b/repotest/repotest.go new file mode 100644 index 00000000..87031a4d --- /dev/null +++ b/repotest/repotest.go @@ -0,0 +1,200 @@ +// Package repotest contains various test helpers for tests that work with code +// relying on plugin Git repos and the asdf plugin index +// +// Three main actions: +// +// * Install plugin index repo into asdf (index contains records that point to +// local plugins defined by this package) +// * Install plugin into asdf data dir +// * Create local plugin repo that can be cloned into asdf +package repotest + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + cp "github.com/otiai10/copy" +) + +const fixturesDir = "fixtures" + +// Setup copies all files into place and initializes all repos for any Go test +// that needs either plugin repos or the plugin index repo. +func Setup(asdfDataDir string) error { + if err := InstallPluginIndex(asdfDataDir); err != nil { + return err + } + + return nil +} + +// GeneratePlugin copies in the specified plugin fixture into a test directory +// and initializes a Git repo for it so it can be installed by asdf. +func GeneratePlugin(fixtureName, asdfDataDir, pluginName string) (string, error) { + root, err := getModuleRoot() + if err != nil { + return "", err + } + + fixturesDir := filepath.Join(asdfDataDir, fixturesDir) + return generatePluginInDir(root, fixtureName, fixturesDir, pluginName) +} + +// InstallPluginIndex generates and installs a plugin index Git repo inside of +// the provided asdf data directory. +func InstallPluginIndex(asdfDataDir string) error { + root, err := getModuleRoot() + if err != nil { + return err + } + + // Copy in plugin index + source := filepath.Join(root, "test/fixtures/dummy_plugins_repo") + return cp.Copy(source, filepath.Join(asdfDataDir, "plugin-index")) +} + +// GeneratePluginIndex generates a mock plugin index Git repo inside the given +// directory. +func GeneratePluginIndex(asdfDataDir string) (string, error) { + root, err := getModuleRoot() + if err != nil { + return "", err + } + + // Copy in plugin index + source := filepath.Join(root, "test/fixtures/dummy_plugins_repo") + destination := filepath.Join(asdfDataDir, fixturesDir, "plugin-index") + err = cp.Copy(source, destination) + if err != nil { + return destination, fmt.Errorf("unable to copy in plugin index: %w", err) + } + + // Generate git repo for plugin + return createGitRepo(destination) +} + +func generatePluginInDir(root, fixtureName, outputDir, pluginName string) (string, error) { + // Copy in plugin files into output dir + pluginPath, err := copyInPlugin(root, fixtureName, outputDir, pluginName) + if err != nil { + return pluginPath, fmt.Errorf("unable to copy in plugin files: %w", err) + } + + // Generate git repo for plugin + return createGitRepo(pluginPath) +} + +func getModuleRoot() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("unable to get current working directory: %w", err) + } + + root := findModuleRoot(cwd) + return root, nil +} + +func createGitRepo(location string) (string, error) { + // Definitely some opportunities to refactor here. This code might be + // simplified by switching to the Go git library + err := runCmd("git", "-C", location, "init", "-q") + if err != nil { + return location, err + } + + err = runCmd("git", "-C", location, "config", "user.name", "\"Test\"") + if err != nil { + return location, err + } + + err = runCmd("git", "-C", location, "config", "user.email", "\"test@example.com\"") + if err != nil { + return location, err + } + + err = runCmd("git", "-C", location, "add", "-A") + if err != nil { + return location, err + } + + err = runCmd("git", "-C", location, "commit", "-q", "-m", "init repo") + if err != nil { + return location, err + } + + err = runCmd("touch", filepath.Join(location, "README.md")) + if err != nil { + return location, err + } + + err = runCmd("git", "-C", location, "add", "-A") + if err != nil { + return location, err + } + + err = runCmd("git", "-C", location, "commit", "-q", "-m", "add readme") + if err != nil { + return location, err + } + + // kind of ugly but I want a remote with a valid path so I use the same + // location as the remote. Probably should refactor + err = runCmd("git", "-C", location, "remote", "add", "origin", location) + if err != nil { + return location, err + } + + return location, err +} + +func copyInPlugin(root, name, destination, newName string) (string, error) { + source := filepath.Join(root, "test/fixtures/", name) + dest := filepath.Join(destination, newName) + return dest, cp.Copy(source, dest) +} + +// Taken from https://github.com/golang/go/blob/9e3b1d53a012e98cfd02de2de8b1bd53522464d4/src/cmd/go/internal/modload/init.go#L1504C1-L1522C2 because that function is in an internal module +// and I can't rely on it. +func findModuleRoot(dir string) (roots string) { + if dir == "" { + panic("dir not set") + } + dir = filepath.Clean(dir) + + // Look for enclosing go.mod. + for { + if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() { + return dir + } + d := filepath.Dir(dir) + if d == dir { + break + } + dir = d + } + return "" +} + +// helper function to make running commands easier +func runCmd(cmdName string, args ...string) error { + cmd := exec.Command(cmdName, args...) + + // Capture stdout and stderr + var stdout strings.Builder + var stderr strings.Builder + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + // If command fails print both stderr and stdout + fmt.Println("stdout:", stdout.String()) + fmt.Println("stderr:", stderr.String()) + return err + } + + return nil +}