diff --git a/cmd/cmd.go b/cmd/cmd.go index 71f3bd1c..42290934 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -20,6 +20,7 @@ import ( "asdf/internal/hook" "asdf/internal/info" "asdf/internal/installs" + "asdf/internal/pluginindex" "asdf/internal/plugins" "asdf/internal/resolve" "asdf/internal/shims" @@ -173,6 +174,14 @@ func Execute(version string) { Action: func(cCtx *cli.Context) error { return pluginListCommand(cCtx, logger) }, + Subcommands: []*cli.Command{ + { + Name: "all", + Action: func(_ *cli.Context) error { + return pluginListAllCommand(logger) + }, + }, + }, }, { Name: "remove", @@ -628,6 +637,68 @@ func pluginListCommand(cCtx *cli.Context, logger *log.Logger) error { return nil } +func pluginListAllCommand(logger *log.Logger) error { + conf, err := config.LoadConfig() + if err != nil { + logger.Printf("error loading config: %s", err) + return err + } + + disableRepo, err := conf.DisablePluginShortNameRepository() + if err != nil { + logger.Printf("unable to check config") + return err + } + if disableRepo { + logger.Printf("Short-name plugin repository is disabled") + os.Exit(1) + return nil + } + + lastCheckDuration := 0 + // We don't care about errors here as we can use the default value + checkDuration, _ := conf.PluginRepositoryLastCheckDuration() + + if !checkDuration.Never { + lastCheckDuration = checkDuration.Every + } + + index := pluginindex.Build(conf.DataDir, conf.PluginIndexURL, false, lastCheckDuration) + availablePlugins, err := index.Get() + if err != nil { + logger.Printf("error loading plugin index: %s", err) + return err + } + + installedPlugins, err := plugins.List(conf, true, false) + if err != nil { + logger.Printf("error loading plugin list: %s", err) + return err + } + + w := tabwriter.NewWriter(os.Stdout, 15, 0, 1, ' ', 0) + for _, availablePlugin := range availablePlugins { + if pluginInstalled(availablePlugin, installedPlugins) { + fmt.Fprintf(w, "%s\t\t*%s\n", availablePlugin.Name, availablePlugin.URL) + } else { + fmt.Fprintf(w, "%s\t\t%s\n", availablePlugin.Name, availablePlugin.URL) + } + } + w.Flush() + + return nil +} + +func pluginInstalled(plugin pluginindex.Plugin, installedPlugins []plugins.Plugin) bool { + for _, installedPlugin := range installedPlugins { + if installedPlugin.Name == plugin.Name && installedPlugin.URL == plugin.URL { + return true + } + } + + return false +} + func infoCommand(conf config.Config, version string) error { return info.Print(conf, version) } diff --git a/internal/config/config.go b/internal/config/config.go index b1545861..f95a4034 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ const ( dataDirDefault = "~/.asdf" configFileDefault = "~/.asdfrc" defaultToolVersionsFilenameDefault = ".tool-versions" + defaultPluginIndexURL = "https://github.com/asdf-vm/asdf-plugins.git" ) /* PluginRepoCheckDuration represents the remote plugin repo check duration @@ -40,7 +41,8 @@ type Config struct { DataDir string `env:"ASDF_DATA_DIR, overwrite"` ForcePrepend bool `env:"ASDF_FORCE_PREPEND, overwrite"` // Field that stores the settings struct if it is loaded - Settings Settings + Settings Settings + PluginIndexURL string } // Settings is a struct that stores config values from the asdfrc file @@ -62,6 +64,7 @@ func defaultConfig(dataDir, configFile string) *Config { DataDir: dataDir, ConfigFile: configFile, DefaultToolVersionsFilename: defaultToolVersionsFilenameDefault, + PluginIndexURL: defaultPluginIndexURL, } } diff --git a/internal/pluginindex/pluginindex.go b/internal/pluginindex/pluginindex.go index d826bf9f..ae05a097 100644 --- a/internal/pluginindex/pluginindex.go +++ b/internal/pluginindex/pluginindex.go @@ -4,6 +4,7 @@ package pluginindex import ( "fmt" + "io/fs" "os" "path/filepath" "time" @@ -28,6 +29,12 @@ type PluginIndex struct { updateDurationMinutes int } +// Plugin represents a plugin listed on a plugin index. +type Plugin struct { + Name string + URL string +} + // Build returns a complete PluginIndex struct with default values set func Build(dataDir string, URL string, disableUpdate bool, updateDurationMinutes int) PluginIndex { directory := filepath.Join(dataDir, pluginIndexDir) @@ -45,6 +52,16 @@ func New(directory, url string, disableUpdate bool, updateDurationMinutes int, r } } +// Get returns a slice of all available plugins +func (p PluginIndex) Get() (plugins []Plugin, err error) { + _, err = p.Refresh() + if err != nil { + return plugins, err + } + + return getPlugins(p.directory) +} + // Refresh may update the plugin repo if it hasn't been updated in longer // than updateDurationMinutes. If the plugin repo needs to be updated the // repo will be invoked to perform the actual Git pull. @@ -145,3 +162,23 @@ func readPlugin(dir, name string) (string, error) { return pluginInfo.Section("").Key("repository").String(), nil } + +func getPlugins(dir string) (plugins []Plugin, err error) { + files, err := os.ReadDir(filepath.Join(dir, "plugins")) + if _, ok := err.(*fs.PathError); ok { + return plugins, nil + } + + for _, file := range files { + if !file.IsDir() { + url, err := readPlugin(dir, file.Name()) + if err != nil { + return plugins, err + } + + plugins = append(plugins, Plugin{Name: file.Name(), URL: url}) + } + } + + return plugins, err +} diff --git a/internal/pluginindex/pluginindex_test.go b/internal/pluginindex/pluginindex_test.go index 5b4d45e6..a813ba8a 100644 --- a/internal/pluginindex/pluginindex_test.go +++ b/internal/pluginindex/pluginindex_test.go @@ -83,6 +83,17 @@ func writeMockPluginFile(dir, pluginName, pluginURL string) error { return nil } +func TestGet(t *testing.T) { + t.Run("returns populated slice of plugins when plugins exist in directory", func(t *testing.T) { + dir := t.TempDir() + + pluginIndex := New(dir, mockIndexURL, true, 0, &MockIndex{Directory: dir}) + plugins, err := pluginIndex.Get() + assert.Nil(t, err) + assert.Equal(t, plugins, []Plugin{{Name: "elixir", URL: "https://github.com/asdf-vm/asdf-elixir.git"}}) + }) +} + 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() diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go index 12ab3a97..dffde982 100644 --- a/internal/plugins/plugins.go +++ b/internal/plugins/plugins.go @@ -272,7 +272,7 @@ func Add(config config.Config, pluginName, pluginURL string) error { lastCheckDuration = checkDuration.Every } - index := pluginindex.Build(config.DataDir, "https://github.com/asdf-vm/asdf-plugins.git", false, lastCheckDuration) + index := pluginindex.Build(config.DataDir, config.PluginIndexURL, false, lastCheckDuration) var err error pluginURL, err = index.GetPluginSourceURL(pluginName) if err != nil { diff --git a/main_test.go b/main_test.go index e4f79246..a0c42ef3 100644 --- a/main_test.go +++ b/main_test.go @@ -51,9 +51,9 @@ func TestBatsTests(t *testing.T) { // runBatsFile(t, dir, "plugin_extension_command.bats") //}) - //t.Run("plugin_list_all_command", func(t *testing.T) { - // runBatsFile(t, dir, "plugin_list_all_command.bats") - //}) + t.Run("plugin_list_all_command", func(t *testing.T) { + runBatsFile(t, dir, "plugin_list_all_command.bats") + }) t.Run("plugin_remove_command", func(t *testing.T) { runBatsFile(t, dir, "plugin_remove_command.bats") diff --git a/test/plugin_list_all_command.bats b/test/plugin_list_all_command.bats index 6fa8bcb2..99f28bfd 100644 --- a/test/plugin_list_all_command.bats +++ b/test/plugin_list_all_command.bats @@ -26,15 +26,13 @@ teardown() { @test "plugin_list_all should sync repo when check_duration set to 0" { export ASDF_CONFIG_DEFAULT_FILE="$HOME/.asdfrc" echo 'plugin_repository_last_check_duration = 0' >"$ASDF_CONFIG_DEFAULT_FILE" - local expected_plugin_repo_sync="updating plugin repository..." local expected_plugins_list="\ bar http://example.com/bar -dummy *http://example.com/dummy +dummy http://example.com/dummy foo http://example.com/foo" run asdf plugin list all [ "$status" -eq 0 ] - [[ "$output" == *"$expected_plugin_repo_sync"* ]] [[ "$output" == *"$expected_plugins_list"* ]] } @@ -43,7 +41,7 @@ foo http://example.com/foo" echo 'plugin_repository_last_check_duration = 10' >"$ASDF_CONFIG_DEFAULT_FILE" local expected="\ bar http://example.com/bar -dummy *http://example.com/dummy +dummy http://example.com/dummy foo http://example.com/foo" run asdf plugin list all @@ -56,7 +54,7 @@ foo http://example.com/foo" echo 'plugin_repository_last_check_duration = never' >"$ASDF_CONFIG_DEFAULT_FILE" local expected="\ bar http://example.com/bar -dummy *http://example.com/dummy +dummy http://example.com/dummy foo http://example.com/foo" run asdf plugin list all @@ -67,7 +65,7 @@ foo http://example.com/foo" @test "plugin_list_all list all plugins in the repository" { local expected="\ bar http://example.com/bar -dummy *http://example.com/dummy +dummy http://example.com/dummy foo http://example.com/foo" run asdf plugin list all diff --git a/test/test_helpers.bash b/test/test_helpers.bash index a78ad5cb..dc5c7de9 100644 --- a/test/test_helpers.bash +++ b/test/test_helpers.bash @@ -70,12 +70,13 @@ install_mock_plugin_repo() { init_git_repo() { location="$1" + remote="${2:-"https://asdf-vm.com/fake-repo"}" git -C "${location}" init -q git -C "${location}" config user.name "Test" git -C "${location}" config user.email "test@example.com" git -C "${location}" add -A git -C "${location}" commit -q -m "asdf ${plugin_name} plugin" - git -C "${location}" remote add origin "https://asdf-vm.com/fake-repo" + git -C "${location}" remote add origin "$remote" } install_mock_plugin_version() { @@ -127,6 +128,9 @@ clean_asdf_dir() { } setup_repo() { - cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_plugins_repo" "$ASDF_DIR/repository" + cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_plugins_repo" "$ASDF_DIR/plugin-index" + cp -r "$BATS_TEST_DIRNAME/fixtures/dummy_plugins_repo" "$ASDF_DIR/plugin-index-2" + init_git_repo "$ASDF_DIR/plugin-index-2" + init_git_repo "$ASDF_DIR/plugin-index" "$ASDF_DIR/plugin-index-2" touch "$(asdf_dir)/tmp/repo-updated" }