Merge pull request #29 from asdf-vm/tb/plugin-list-command

feat(golang-rewrite): create plugin list command
This commit is contained in:
Trevor Brown 2024-03-07 08:54:23 -05:00 committed by Trevor Brown
commit 974b11b0fe
4 changed files with 301 additions and 33 deletions

View File

@ -48,6 +48,7 @@ func Execute() {
conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
return err
}
return pluginAddCommand(cCtx, conf, logger, args.Get(0), args.Get(1))
@ -55,8 +56,18 @@ func Execute() {
},
&cli.Command{
Name: "list",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "urls",
Usage: "Show URLs",
},
&cli.BoolFlag{
Name: "refs",
Usage: "Show Refs",
},
},
Action: func(cCtx *cli.Context) error {
return nil
return pluginListCommand(cCtx, logger)
},
},
&cli.Command{
@ -109,3 +120,38 @@ func pluginAddCommand(cCtx *cli.Context, conf config.Config, logger *log.Logger,
}
return nil
}
func pluginListCommand(cCtx *cli.Context, logger *log.Logger) error {
urls := cCtx.Bool("urls")
refs := cCtx.Bool("refs")
conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
return err
}
plugins, err := plugins.List(conf, urls, refs)
if err != nil {
logger.Printf("error loading plugin list: %s", err)
return err
}
// TODO: Add some sort of presenter logic in another file so we
// don't clutter up this cmd code with conditional presentation
// logic
for _, plugin := range plugins {
if urls && refs {
logger.Printf("%s\t\t%s\t%s\n", plugin.Name, plugin.Url, plugin.Ref)
} else if refs {
logger.Printf("%s\t\t%s\n", plugin.Name, plugin.Ref)
} else if urls {
logger.Printf("%s\t\t%s\n", plugin.Name, plugin.Url)
} else {
logger.Printf("%s\n", plugin.Name)
}
}
return nil
}

View File

@ -20,113 +20,113 @@ func TestBatsTests(t *testing.T) {
defer os.RemoveAll(dir)
// Build asdf and put in temp directory
build_asdf(t, dir)
buildAsdf(t, dir)
// Run tests with the asdf binary in the temp directory
// Uncomment these as they are implemented
//t.Run("current_command", func(t *testing.T) {
// run_bats_file(t, dir, "current_command.bats")
// runBatsFile(t, dir, "current_command.bats")
//})
//t.Run("get_asdf_config_value", func(t *testing.T) {
// run_bats_file(t, dir, "get_asdf_config_value.bats")
// runBatsFile(t, dir, "get_asdf_config_value.bats")
//})
//t.Run("help_command", func(t *testing.T) {
// run_bats_file(t, dir, "help_command.bats")
// runBatsFile(t, dir, "help_command.bats")
//})
//t.Run("info_command", func(t *testing.T) {
// run_bats_file(t, dir, "info_command.bats")
// runBatsFile(t, dir, "info_command.bats")
//})
//t.Run("install_command", func(t *testing.T) {
// run_bats_file(t, dir, "install_command.bats")
// runBatsFile(t, dir, "install_command.bats")
//})
//t.Run("latest_command", func(t *testing.T) {
// run_bats_file(t, dir, "latest_command.bats")
// runBatsFile(t, dir, "latest_command.bats")
//})
//t.Run("list_command", func(t *testing.T) {
// run_bats_file(t, dir, "list_command.bats")
// runBatsFile(t, dir, "list_command.bats")
//})
//t.Run("plugin_add_command", func(t *testing.T) {
// run_bats_file(t, dir, "plugin_add_command.bats")
// runBatsFile(t, dir, "plugin_add_command.bats")
//})
//t.Run("plugin_extension_command", func(t *testing.T) {
// run_bats_file(t, dir, "plugin_extension_command.bats")
// runBatsFile(t, dir, "plugin_extension_command.bats")
//})
//t.Run("plugin_list_all_command", func(t *testing.T) {
// run_bats_file(t, dir, "plugin_list_all_command.bats")
// runBatsFile(t, dir, "plugin_list_all_command.bats")
//})
//t.Run("plugin_remove_command", func(t *testing.T) {
// run_bats_file(t, dir, "plugin_remove_command.bats")
// runBatsFile(t, dir, "plugin_remove_command.bats")
//})
//t.Run("plugin_test_command", func(t *testing.T) {
// run_bats_file(t, dir, "plugin_test_command.bats")
// runBatsFile(t, dir, "plugin_test_command.bats")
//})
//t.Run("plugin_update_command", func(t *testing.T) {
// run_bats_file(t, dir, "plugin_update_command.bats")
// runBatsFile(t, dir, "plugin_update_command.bats")
//})
//t.Run("remove_command", func(t *testing.T) {
// run_bats_file(t, dir, "remove_command.bats")
// runBatsFile(t, dir, "remove_command.bats")
//})
//t.Run("reshim_command", func(t *testing.T) {
// run_bats_file(t, dir, "reshim_command.bats")
// runBatsFile(t, dir, "reshim_command.bats")
//})
//t.Run("reshim_command", func(t *testing.T) {
// run_bats_file(t, dir, "reshim_command.bats")
// runBatsFile(t, dir, "reshim_command.bats")
//})
//t.Run("shim_env_command", func(t *testing.T) {
// run_bats_file(t, dir, "shim_env_command.bats")
// runBatsFile(t, dir, "shim_env_command.bats")
//})
//t.Run("shim_exec", func(t *testing.T) {
// run_bats_file(t, dir, "shim_exec.bats")
// runBatsFile(t, dir, "shim_exec.bats")
//})
//t.Run("shim_versions_command", func(t *testing.T) {
// run_bats_file(t, dir, "shim_versions_command.bats")
// runBatsFile(t, dir, "shim_versions_command.bats")
//})
//t.Run("shim_versions_command", func(t *testing.T) {
// run_bats_file(t, dir, "shim_versions_command.bats")
// runBatsFile(t, dir, "shim_versions_command.bats")
//})
//t.Run("uninstall_command", func(t *testing.T) {
// run_bats_file(t, dir, "uninstall_command.bats")
// runBatsFile(t, dir, "uninstall_command.bats")
//})
//t.Run("update_command", func(t *testing.T) {
// run_bats_file(t, dir, "update_command.bats")
// runBatsFile(t, dir, "update_command.bats")
//})
//t.Run("version_commands", func(t *testing.T) {
// run_bats_file(t, dir, "version_commands.bats")
// runBatsFile(t, dir, "version_commands.bats")
//})
//t.Run("where_command", func(t *testing.T) {
// run_bats_file(t, dir, "where_command.bats")
// runBatsFile(t, dir, "where_command.bats")
//})
//t.Run("which_command", func(t *testing.T) {
// run_bats_file(t, dir, "which_command.bats")
// runBatsFile(t, dir, "which_command.bats")
//})
}
func run_bats_file(t *testing.T, dir, filename string) {
func runBatsFile(t *testing.T, dir, filename string) {
t.Helper()
cmd := exec.Command("bats", "--verbose-run", fmt.Sprintf("test/%s", filename))
@ -138,9 +138,9 @@ func run_bats_file(t *testing.T, dir, filename string) {
cmd.Stderr = &stderr
// Add dir to asdf test variables
asdf_test_home := fmt.Sprintf("HOME=%s/.asdf/", dir)
asdf_bin_path := fmt.Sprintf("ASDF_BIN=%s", dir)
cmd.Env = append(cmd.Environ(), asdf_bin_path, asdf_test_home)
asdfTestHome := fmt.Sprintf("HOME=%s/.asdf/", dir)
asdfBinPath := fmt.Sprintf("ASDF_BIN=%s", dir)
cmd.Env = append(cmd.Environ(), asdfBinPath, asdfTestHome)
err := cmd.Run()
if err != nil {
@ -153,7 +153,7 @@ func run_bats_file(t *testing.T, dir, filename string) {
}
}
func build_asdf(t *testing.T, dir string) {
func buildAsdf(t *testing.T, dir string) {
cmd := exec.Command("go", "build", "-o", dir)
err := cmd.Run()

View File

@ -15,6 +15,69 @@ const dataDirPlugins = "plugins"
const invalidPluginNameMsg = "'%q' is invalid. Name may only contain lowercase letters, numbers, '_', and '-'"
const pluginAlreadyExists = "plugin named %q already added"
type Plugin struct {
Name string
Dir string
Ref string
Url string
}
func List(config config.Config, urls, refs bool) (plugins []Plugin, err error) {
pluginsDir := PluginsDataDirectory(config.DataDir)
files, err := os.ReadDir(pluginsDir)
if err != nil {
return plugins, err
}
for _, file := range files {
if file.IsDir() {
if refs || urls {
var url string
var refString string
location := filepath.Join(pluginsDir, file.Name())
repo, err := git.PlainOpen(location)
// TODO: Improve these error messages
if err != nil {
return plugins, err
}
if refs {
ref, err := repo.Head()
refString = ref.Hash().String()
if err != nil {
return plugins, err
}
}
if urls {
remotes, err := repo.Remotes()
url = remotes[0].Config().URLs[0]
if err != nil {
return plugins, err
}
}
plugins = append(plugins, Plugin{
Name: file.Name(),
Dir: location,
Url: url,
Ref: refString,
})
} else {
plugins = append(plugins, Plugin{
Name: file.Name(),
Dir: filepath.Join(pluginsDir, file.Name()),
})
}
}
}
return plugins, nil
}
func PluginAdd(config config.Config, pluginName, pluginUrl string) error {
err := validatePluginName(pluginName)
@ -64,7 +127,11 @@ func PluginExists(dataDir, pluginName string) (bool, error) {
}
func PluginDirectory(dataDir, pluginName string) string {
return filepath.Join(dataDir, dataDirPlugins, pluginName)
return filepath.Join(PluginsDataDirectory(dataDir), pluginName)
}
func PluginsDataDirectory(dataDir string) string {
return filepath.Join(dataDir, dataDirPlugins)
}
func validatePluginName(name string) error {

View File

@ -2,7 +2,10 @@ package plugins
import (
"asdf/config"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
@ -15,6 +18,60 @@ const (
testPluginName = "lua"
)
func TestList(t *testing.T) {
testDataDir := t.TempDir()
conf := config.Config{DataDir: testDataDir}
testRepo, err := installMockPluginRepo(testDataDir, testPluginName)
assert.Nil(t, err)
err = PluginAdd(conf, testPluginName, testRepo)
assert.Nil(t, err)
t.Run("when urls and refs are set to false returns plugin names", func(t *testing.T) {
plugins, err := List(conf, false, false)
assert.Nil(t, err)
plugin := plugins[0]
assert.Equal(t, "lua", plugin.Name)
assert.NotZero(t, plugin.Dir)
assert.Zero(t, plugin.Url)
assert.Zero(t, plugin.Ref)
})
t.Run("when urls is set to true returns plugins with repo urls set", func(t *testing.T) {
plugins, err := List(conf, true, false)
assert.Nil(t, err)
plugin := plugins[0]
assert.Equal(t, "lua", plugin.Name)
assert.NotZero(t, plugin.Dir)
assert.Zero(t, plugin.Ref)
assert.NotZero(t, plugin.Url)
})
t.Run("when refs is set to true returns plugins with current repo refs set", func(t *testing.T) {
plugins, err := List(conf, false, true)
assert.Nil(t, err)
plugin := plugins[0]
assert.Equal(t, "lua", plugin.Name)
assert.NotZero(t, plugin.Dir)
assert.NotZero(t, plugin.Ref)
assert.Zero(t, plugin.Url)
})
t.Run("when refs and urls are both set to true returns plugins with both set", func(t *testing.T) {
plugins, err := List(conf, true, true)
assert.Nil(t, err)
plugin := plugins[0]
assert.Equal(t, "lua", plugin.Name)
assert.NotZero(t, plugin.Dir)
assert.NotZero(t, plugin.Ref)
assert.NotZero(t, plugin.Url)
})
}
func TestPluginAdd(t *testing.T) {
testDataDir := t.TempDir()
@ -178,3 +235,101 @@ 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\"", name))
return location, err
}
// 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
}
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 ""
}