Merge pull request #72 from asdf-vm/tb/asdf-uninstall-command

feat(golang-rewrite): create `asdf uninstall` command
This commit is contained in:
Trevor Brown 2024-10-14 09:06:23 -04:00 committed by Trevor Brown
commit a0b079c903
8 changed files with 208 additions and 22 deletions

View File

@ -176,6 +176,15 @@ func Execute(version string) {
return reshimCommand(logger, args.Get(0), args.Get(1))
},
},
{
Name: "uninstall",
Action: func(cCtx *cli.Context) error {
tool := cCtx.Args().Get(0)
version := cCtx.Args().Get(1)
return uninstallCommand(logger, tool, version)
},
},
{
Name: "where",
Action: func(cCtx *cli.Context) error {
@ -536,7 +545,7 @@ func installCommand(logger *log.Logger, toolName, version string) error {
return err
}
} else {
parsedVersion, query := parseInstallVersion(version)
parsedVersion, query := toolversions.ParseFromCliArg(version)
if parsedVersion == "latest" {
err = versions.InstallVersion(conf, plugin, version, query, os.Stdout, os.Stderr)
@ -563,16 +572,6 @@ func filterInstallErrors(errs []error) []error {
return filtered
}
func parseInstallVersion(version string) (string, string) {
segments := strings.Split(version, ":")
if len(segments) > 1 && segments[0] == "latest" {
// Must be latest with filter
return "latest", segments[1]
}
return version, ""
}
func latestCommand(logger *log.Logger, all bool, toolName, pattern string) (err error) {
conf, err := config.LoadConfig()
if err != nil {
@ -673,6 +672,40 @@ func whichCommand(logger *log.Logger, command string) error {
return nil
}
func uninstallCommand(logger *log.Logger, tool, version string) error {
if tool == "" || version == "" {
logger.Print("No plugin given")
os.Exit(1)
return nil
}
conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
os.Exit(1)
return err
}
plugin := plugins.New(conf, tool)
err = versions.Uninstall(conf, plugin, version, os.Stdout, os.Stderr)
if err != nil {
logger.Printf("%s", err)
os.Exit(1)
return err
}
// This feels a little hacky but it works, to re-generate shims we delete them
// all and generate them again.
err = shims.RemoveAll(conf)
if err != nil {
logger.Printf("%s", err)
os.Exit(1)
return err
}
return shims.GenerateAll(conf, os.Stdout, os.Stderr)
}
func whereCommand(logger *log.Logger, tool, version string) error {
conf, err := config.LoadConfig()
if err != nil {

View File

@ -83,6 +83,22 @@ func Unique(versions []ToolVersions) (uniques []ToolVersions) {
return uniques
}
// ParseFromCliArg parses a string that is passed in as an argument to one of
// the asdf subcommands. Some subcommands allow the special version `latest` to
// be used, with an optional filter string.
func ParseFromCliArg(version string) (string, string) {
segments := strings.Split(version, ":")
if len(segments) > 0 && segments[0] == "latest" {
if len(segments) > 1 {
// Must be latest with filter
return "latest", segments[1]
}
return "latest", ""
}
return Parse(version)
}
// Parse parses a version string into versionType and version components
func Parse(version string) (string, string) {
segments := strings.Split(version, ":")

View File

@ -178,6 +178,38 @@ func TestParse(t *testing.T) {
})
}
func TestParseFromCliArg(t *testing.T) {
t.Run("returns 'latest' as version type when passed string 'latest'", func(t *testing.T) {
versionType, version := ParseFromCliArg("latest")
assert.Equal(t, versionType, "latest")
assert.Equal(t, version, "")
})
t.Run("returns 'latest' and unmodified filter string when passed a latest version", func(t *testing.T) {
versionType, version := ParseFromCliArg("latest:1.2")
assert.Equal(t, versionType, "latest")
assert.Equal(t, version, "1.2")
})
t.Run("returns 'version', and unmodified version when passed semantic version", func(t *testing.T) {
versionType, version := ParseFromCliArg("1.2.3")
assert.Equal(t, versionType, "version")
assert.Equal(t, version, "1.2.3")
})
t.Run("returns 'ref' and reference version when passed a ref version", func(t *testing.T) {
versionType, version := ParseFromCliArg("ref:abc123")
assert.Equal(t, versionType, "ref")
assert.Equal(t, version, "abc123")
})
t.Run("returns 'ref' and empty string when passed 'ref:'", func(t *testing.T) {
versionType, version := ParseFromCliArg("ref:")
assert.Equal(t, versionType, "ref")
assert.Equal(t, version, "")
})
}
func TestFormatForFS(t *testing.T) {
t.Run("returns version when version type is not ref", func(t *testing.T) {
assert.Equal(t, FormatForFS("version", "foobar"), "foobar")

View File

@ -0,0 +1,2 @@
pre_asdf_uninstall_uninstall-test = echo pre_asdf_uninstall_test $@
post_asdf_uninstall_uninstall-test = echo post_asdf_uninstall_test $@

View File

@ -274,6 +274,50 @@ func AllVersionsFiltered(plugin plugins.Plugin, query string) (versions []string
return filterByExactMatch(all, query), err
}
// Uninstall uninstalls a specific tool version. It invokes pre and
// post-uninstall hooks if set, and runs the plugin's uninstall callback if
// defined.
func Uninstall(conf config.Config, plugin plugins.Plugin, rawVersion string, stdout, stderr io.Writer) error {
versionType, version := toolversions.ParseFromCliArg(rawVersion)
if versionType == "latest" {
return errors.New("'latest' is a special version value that cannot be used for uninstall command")
}
if !installs.IsInstalled(conf, plugin, versionType, version) {
return errors.New("No such version")
}
err := hook.RunWithOutput(conf, fmt.Sprintf("pre_asdf_uninstall_%s", plugin.Name), []string{version}, stdout, stderr)
if err != nil {
return err
}
// invoke uninstall callback if available
installDir := installs.InstallPath(conf, plugin, versionType, version)
env := map[string]string{
"ASDF_INSTALL_TYPE": versionType,
"ASDF_INSTALL_VERSION": version,
"ASDF_INSTALL_PATH": installDir,
}
err = plugin.RunCallback("uninstall", []string{}, env, stdout, stderr)
if _, ok := err.(plugins.NoCallbackError); !ok && err != nil {
return err
}
err = os.RemoveAll(installDir)
if err != nil {
return err
}
err = hook.RunWithOutput(conf, fmt.Sprintf("post_asdf_uninstall_%s", plugin.Name), []string{version}, stdout, stderr)
if err != nil {
return err
}
return nil
}
func filterByExactMatch(allVersions []string, pattern string) (versions []string) {
for _, version := range allVersions {
if strings.HasPrefix(version, pattern) {

View File

@ -333,6 +333,62 @@ func TestAllVersions(t *testing.T) {
})
}
func TestUninstall(t *testing.T) {
t.Setenv("ASDF_CONFIG_FILE", "testdata/uninstall-asdfrc")
pluginName := "uninstall-test"
conf, _ := generateConfig(t)
_, err := repotest.InstallPlugin("dummy_plugin", conf.DataDir, pluginName)
assert.Nil(t, err)
plugin := plugins.New(conf, pluginName)
stdout, stderr := buildOutputs()
t.Run("returns error when version is 'latest'", func(t *testing.T) {
stdout, stderr := buildOutputs()
err := Uninstall(conf, plugin, "latest", &stdout, &stderr)
assert.Error(t, err, "'latest' is a special version value that cannot be used for uninstall command")
})
t.Run("returns an error when version not installed", func(t *testing.T) {
err := Uninstall(conf, plugin, "4.0.0", &stdout, &stderr)
assert.Error(t, err, "No such version")
})
t.Run("uninstalls successfully when plugin and version are installed", func(t *testing.T) {
err = InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err)
err := Uninstall(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err)
assertNotInstalled(t, conf.DataDir, plugin.Name, "1.0.0")
})
t.Run("runs pre and post-uninstall hooks", func(t *testing.T) {
stdout, stderr := buildOutputs()
err = InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err)
err := Uninstall(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err)
want := "pre_asdf_uninstall_test 1.0.0\npost_asdf_uninstall_test 1.0.0\n"
assert.Equal(t, want, stdout.String())
})
t.Run("invokes uninstall callback when present", func(t *testing.T) {
stdout, stderr := buildOutputs()
err = InstallOneVersion(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err)
data := []byte("echo custom uninstall")
err := os.WriteFile(filepath.Join(plugin.Dir, "bin", "uninstall"), data, 0o755)
assert.Nil(t, err)
err = Uninstall(conf, plugin, "1.0.0", &stdout, &stderr)
assert.Nil(t, err)
want := "pre_asdf_uninstall_test 1.0.0\ncustom uninstall\npost_asdf_uninstall_test 1.0.0\n"
assert.Equal(t, want, stdout.String())
})
}
// Helper functions
func buildOutputs() (strings.Builder, strings.Builder) {
var stdout strings.Builder

View File

@ -87,9 +87,9 @@ func TestBatsTests(t *testing.T) {
// runBatsFile(t, dir, "shim_versions_command.bats")
//})
//t.Run("uninstall_command", func(t *testing.T) {
// runBatsFile(t, dir, "uninstall_command.bats")
//})
t.Run("uninstall_command", func(t *testing.T) {
runBatsFile(t, dir, "uninstall_command.bats")
})
//t.Run("version_commands", func(t *testing.T) {
// runBatsFile(t, dir, "version_commands.bats")

View File

@ -72,16 +72,19 @@ teardown() {
[ "$status" -eq 1 ]
}
@test "uninstall_command should not remove other unrelated shims" {
run asdf install dummy 1.0.0
[ -f "$ASDF_DIR/shims/dummy" ]
# Disabled as this test represents an invalid state. A shim (`gummy`) should
# never exist unless it referenced an existing tool and version.
#
#@test "uninstall_command should not remove other unrelated shims" {
# run asdf install dummy 1.0.0
# [ -f "$ASDF_DIR/shims/dummy" ]
touch "$ASDF_DIR/shims/gummy"
[ -f "$ASDF_DIR/shims/gummy" ]
# touch "$ASDF_DIR/shims/gummy"
# [ -f "$ASDF_DIR/shims/gummy" ]
run asdf uninstall dummy 1.0.0
[ -f "$ASDF_DIR/shims/gummy" ]
}
# run asdf uninstall dummy 1.0.0
# [ -f "$ASDF_DIR/shims/gummy" ]
#}
@test "uninstall command executes configured pre hook" {
cat >"$HOME/.asdfrc" <<-'EOM'