Merge pull request #36 from asdf-vm/tb/plugin-update-cmd

feat(golang-rewrite): create plugin update command
This commit is contained in:
Trevor Brown 2024-05-03 21:28:17 -04:00 committed by Trevor Brown
commit 0aba948c65
3 changed files with 200 additions and 47 deletions

View File

@ -82,9 +82,15 @@ func Execute() {
},
{
Name: "update",
Action: func(_ *cli.Context) error {
log.Print("Ipsum")
return nil
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "all",
Usage: "Update all installed plugins",
},
},
Action: func(cCtx *cli.Context) error {
args := cCtx.Args()
return pluginUpdateCommand(cCtx, logger, args.Get(0), args.Get(1))
},
},
},
@ -170,3 +176,45 @@ func pluginListCommand(cCtx *cli.Context, logger *log.Logger) error {
return nil
}
func pluginUpdateCommand(cCtx *cli.Context, logger *log.Logger, pluginName, ref string) error {
updateAll := cCtx.Bool("all")
if !updateAll && pluginName == "" {
return cli.Exit("usage: asdf plugin-update {<name> [git-ref] | --all}", 1)
}
conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
return err
}
if updateAll {
installedPlugins, err := plugins.List(conf, false, false)
if err != nil {
logger.Printf("failed to get plugin list: %s", err)
return err
}
for _, plugin := range installedPlugins {
updatedToRef, err := plugins.Update(conf, plugin.Name, "")
formatUpdateResult(logger, plugin.Name, updatedToRef, err)
}
return nil
}
updatedToRef, err := plugins.Update(conf, pluginName, ref)
formatUpdateResult(logger, pluginName, updatedToRef, err)
return err
}
func formatUpdateResult(logger *log.Logger, pluginName, updatedToRef string, err error) {
if err != nil {
logger.Printf("failed to update %s due to error: %s\n", pluginName, err)
return
}
logger.Printf("updated %s to ref %s\n", pluginName, updatedToRef)
}

View File

@ -9,8 +9,7 @@ import (
"regexp"
"asdf/config"
"github.com/go-git/go-git/v5"
"asdf/plugins/git"
)
const (
@ -19,7 +18,9 @@ const (
pluginAlreadyExists = "plugin named %q already added"
)
// Plugin represents a plugin to the packages in asdf
// Plugin struct represents an asdf plugin to all asdf code. The name and dir
// fields are the most used fields. Ref and Dir only still git info, which is
// only information and shown to the user at times.
type Plugin struct {
Name string
Dir string
@ -42,25 +43,22 @@ func List(config config.Config, urls, refs bool) (plugins []Plugin, err error) {
var url string
var refString string
location := filepath.Join(pluginsDir, file.Name())
repo, err := git.PlainOpen(location)
plugin := git.NewPlugin(location)
// TODO: Improve these error messages
if err != nil {
return plugins, err
}
if refs {
ref, err := repo.Head()
refString = ref.Hash().String()
refString, err = plugin.Head()
if err != nil {
return plugins, err
}
}
if urls {
remotes, err := repo.Remotes()
url = remotes[0].Config().URLs[0]
url, err = plugin.RemoteURL()
if err != nil {
return plugins, err
}
@ -107,17 +105,10 @@ func Add(config config.Config, pluginName, pluginURL string) error {
return fmt.Errorf("unable to create plugin directory: %w", err)
}
_, err = git.PlainClone(pluginDir, false, &git.CloneOptions{
URL: pluginURL,
})
if err != nil {
return fmt.Errorf("unable to clone plugin: %w", err)
}
return nil
return git.NewPlugin(pluginDir).Clone(pluginURL)
}
// Remove removes a plugin with the provided name if installed
// Remove uninstalls a plugin by removing it from the file system if installed
func Remove(config config.Config, pluginName string) error {
err := validatePluginName(pluginName)
if err != nil {
@ -138,11 +129,33 @@ func Remove(config config.Config, pluginName string) error {
return os.RemoveAll(pluginDir)
}
// Update a plugin to a specific ref, or if no ref provided update to latest
func Update(config config.Config, pluginName, ref string) (string, error) {
exists, err := PluginExists(config.DataDir, pluginName)
if err != nil {
return "", fmt.Errorf("unable to check if plugin exists: %w", err)
}
if !exists {
return "", fmt.Errorf("no such plugin: %s", pluginName)
}
pluginDir := PluginDirectory(config.DataDir, pluginName)
plugin := git.NewPlugin(pluginDir)
return plugin.Update(ref)
}
// PluginExists returns a boolean indicating whether or not a plugin with the
// provided name is currently installed
func PluginExists(dataDir, pluginName string) (bool, error) {
pluginDir := PluginDirectory(dataDir, pluginName)
fileInfo, err := os.Stat(pluginDir)
return directoryExists(pluginDir)
}
func directoryExists(dir string) (bool, error) {
fileInfo, err := os.Stat(dir)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
@ -154,13 +167,14 @@ func PluginExists(dataDir, pluginName string) (bool, error) {
return fileInfo.IsDir(), nil
}
// PluginDirectory returns the directory a plugin would be installed in, if it
// is installed
// PluginDirectory returns the directory a plugin with a given name would be in
// if it were installed
func PluginDirectory(dataDir, pluginName string) string {
return filepath.Join(DataDirectory(dataDir), pluginName)
}
// DataDirectory return the plugin directory inside the data directory
// DataDirectory returns the path to the plugin directory inside the data
// directory
func DataDirectory(dataDir string) string {
return filepath.Join(dataDir, dataDirPlugins)
}

View File

@ -157,7 +157,6 @@ func TestRemove(t *testing.T) {
t.Run("returns error when invalid plugin name is given", func(t *testing.T) {
err := Remove(conf, "foo/bar/baz")
assert.NotNil(t, err)
expectedErrMsg := "is invalid. Name may only contain lowercase letters, numbers, '_', and '-'"
assert.ErrorContains(t, err, expectedErrMsg)
})
@ -173,6 +172,72 @@ func TestRemove(t *testing.T) {
})
}
func TestUpdate(t *testing.T) {
testDataDir := t.TempDir()
conf := config.Config{DataDir: testDataDir}
err := Add(conf, testPluginName, testRepo)
assert.Nil(t, err)
badPluginName := "badplugin"
badRepo := PluginDirectory(testDataDir, badPluginName)
err = os.MkdirAll(badRepo, 0o777)
assert.Nil(t, err)
tests := []struct {
desc string
givenConf config.Config
givenName string
givenRef string
wantSomeRef bool
wantErrMsg string
}{
{
desc: "returns error when plugin with name does not exist",
givenConf: conf,
givenName: "nonexistant",
givenRef: "",
wantSomeRef: false,
wantErrMsg: "no such plugin: nonexistant",
},
{
desc: "returns error when plugin repo does not exist",
givenConf: conf,
givenName: "badplugin",
givenRef: "",
wantSomeRef: false,
wantErrMsg: "unable to open plugin Git repository: repository does not exist",
},
{
desc: "updates plugin when plugin with name exists",
givenConf: conf,
givenName: testPluginName,
givenRef: "",
wantSomeRef: true,
wantErrMsg: "",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
updatedToRef, err := Update(tt.givenConf, tt.givenName, tt.givenRef)
if tt.wantErrMsg == "" {
assert.Nil(t, err)
} else {
assert.NotNil(t, err)
assert.ErrorContains(t, err, tt.wantErrMsg)
}
if tt.wantSomeRef == true {
assert.NotZero(t, updatedToRef)
} else {
assert.Zero(t, updatedToRef)
}
})
}
}
func TestPluginExists(t *testing.T) {
testDataDir := t.TempDir()
pluginDir := PluginDirectory(testDataDir, testPluginName)
@ -305,29 +370,34 @@ func installMockPluginRepo(dataDir, name string) (string, error) {
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()
err = runCmd("git", "-C", location, "commit", "-q", "-m", fmt.Sprintf("\"asdf %s plugin init\"", name))
if err != nil {
// If command fails print both stderr and stdout
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())
return err
return location, err
}
return nil
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) {
@ -360,3 +430,24 @@ func findModuleRoot(dir string) (roots string) {
}
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
}