mirror of
https://github.com/asdf-vm/asdf.git
synced 2024-12-23 11:55:13 -07:00
feat(golang-rewrite): create plugins/git package to store plugin Git operations
This commit is contained in:
parent
3ffeec2ea0
commit
325cd3334b
147
plugins/git/git.go
Normal file
147
plugins/git/git.go
Normal file
@ -0,0 +1,147 @@
|
||||
// Package git contains all the Git operations that can be applied to asdf
|
||||
// plugins
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
const remoteName = "origin"
|
||||
|
||||
// Plugin is a struct to contain the plugin Git details
|
||||
type Plugin struct {
|
||||
directory string
|
||||
}
|
||||
|
||||
// PluginOps is an interface for operations that can be applied to asdf plugins.
|
||||
// Right now we only support Git, but in the future we might have other
|
||||
// mechanisms to install and upgrade plugins. asdf doesn't require a plugin
|
||||
// to be a Git repository when asdf uses it, but Git is the only way to install
|
||||
// and upgrade plugins. If other approaches are supported this will be
|
||||
// extracted into the `plugins` module.
|
||||
type PluginOps interface {
|
||||
Clone(pluginURL string) error
|
||||
Head() (string, error)
|
||||
RemoteURL() (string, error)
|
||||
Update(ref string) (string, error)
|
||||
}
|
||||
|
||||
// NewPlugin builds a new Plugin instance
|
||||
func NewPlugin(directory string) Plugin {
|
||||
return Plugin{directory: directory}
|
||||
}
|
||||
|
||||
// Clone installs a plugin via Git
|
||||
func (g Plugin) Clone(pluginURL string) error {
|
||||
_, err := git.PlainClone(g.directory, false, &git.CloneOptions{
|
||||
URL: pluginURL,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to clone plugin: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Head returns the current HEAD ref of the plugin's Git repository
|
||||
func (g Plugin) Head() (string, error) {
|
||||
repo, err := gitOpen(g.directory)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ref, err := repo.Head()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ref.Hash().String(), nil
|
||||
}
|
||||
|
||||
// RemoteURL returns the URL of the default remote for the plugin's Git repository
|
||||
func (g Plugin) RemoteURL() (string, error) {
|
||||
repo, err := gitOpen(g.directory)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
remotes, err := repo.Remotes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return remotes[0].Config().URLs[0], nil
|
||||
}
|
||||
|
||||
// Update updates the plugin's Git repository to the ref if provided, or the
|
||||
// latest commit on the current branch
|
||||
func (g Plugin) Update(ref string) (string, error) {
|
||||
repo, err := gitOpen(g.directory)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var checkoutOptions git.CheckoutOptions
|
||||
|
||||
if ref == "" {
|
||||
// If no ref is provided checkout latest commit on current branch
|
||||
head, err := repo.Head()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !head.Name().IsBranch() {
|
||||
return "", fmt.Errorf("not on a branch, unable to update")
|
||||
}
|
||||
|
||||
// If on a branch checkout the latest version of it from the remote
|
||||
branch := head.Name()
|
||||
ref = branch.String()
|
||||
checkoutOptions = git.CheckoutOptions{Branch: branch, Force: true}
|
||||
} else {
|
||||
// Checkout ref if provided
|
||||
checkoutOptions = git.CheckoutOptions{Hash: plumbing.NewHash(ref), Force: true}
|
||||
}
|
||||
|
||||
fetchOptions := git.FetchOptions{RemoteName: remoteName, Force: true, RefSpecs: []config.RefSpec{
|
||||
config.RefSpec(ref + ":" + ref),
|
||||
}}
|
||||
|
||||
err = repo.Fetch(&fetchOptions)
|
||||
|
||||
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||||
return "", err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = worktree.Checkout(&checkoutOptions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash, err := repo.ResolveRevision(plumbing.Revision("HEAD"))
|
||||
return hash.String(), err
|
||||
}
|
||||
|
||||
func gitOpen(directory string) (*git.Repository, error) {
|
||||
repo, err := git.PlainOpen(directory)
|
||||
|
||||
if err != nil {
|
||||
return repo, fmt.Errorf("unable to open plugin Git repository: %w", err)
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
206
plugins/git/git_test.go
Normal file
206
plugins/git/git_test.go
Normal file
@ -0,0 +1,206 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"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 := NewPlugin(directory)
|
||||
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)
|
||||
|
||||
plugin := NewPlugin(directory)
|
||||
err := plugin.Clone(testRepo)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Assert plugin directory contains Git repo with bin directory
|
||||
_, err = os.ReadDir(directory + "/.git")
|
||||
assert.Nil(t, err)
|
||||
|
||||
entries, err := os.ReadDir(directory + "/bin")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 5, len(entries))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPluginHead(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
directory := filepath.Join(tempDir, testPluginName)
|
||||
|
||||
plugin := NewPlugin(directory)
|
||||
|
||||
err := plugin.Clone(testRepo)
|
||||
assert.Nil(t, err)
|
||||
|
||||
head, err := plugin.Head()
|
||||
assert.Nil(t, err)
|
||||
assert.NotZero(t, head)
|
||||
}
|
||||
|
||||
func TestPluginRemoteURL(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
directory := filepath.Join(tempDir, testPluginName)
|
||||
|
||||
plugin := NewPlugin(directory)
|
||||
|
||||
err := plugin.Clone(testRepo)
|
||||
assert.Nil(t, err)
|
||||
|
||||
url, err := plugin.RemoteURL()
|
||||
assert.Nil(t, err)
|
||||
assert.NotZero(t, url)
|
||||
}
|
||||
|
||||
func TestPluginUpdate(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
directory := filepath.Join(tempDir, testPluginName)
|
||||
|
||||
plugin := NewPlugin(directory)
|
||||
|
||||
err := plugin.Clone(testRepo)
|
||||
assert.Nil(t, err)
|
||||
|
||||
t.Run("returns error when plugin with name does not exist", func(t *testing.T) {
|
||||
nonexistantPath := filepath.Join(tempDir, "nonexistant")
|
||||
nonexistantPlugin := NewPlugin(nonexistantPath)
|
||||
updatedToRef, err := nonexistantPlugin.Update("")
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, updatedToRef, "")
|
||||
expectedErrMsg := "unable to open plugin Git repository: repository does not exist"
|
||||
assert.ErrorContains(t, err, expectedErrMsg)
|
||||
})
|
||||
|
||||
t.Run("returns error when plugin repo does not exist", func(t *testing.T) {
|
||||
badPluginName := "badplugin"
|
||||
badPluginDir := filepath.Join(tempDir, badPluginName)
|
||||
err := os.MkdirAll(badPluginDir, 0777)
|
||||
assert.Nil(t, err)
|
||||
|
||||
badPlugin := NewPlugin(badPluginDir)
|
||||
|
||||
updatedToRef, err := badPlugin.Update("")
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, updatedToRef, "")
|
||||
expectedErrMsg := "unable to open plugin Git repository: repository does not exist"
|
||||
assert.ErrorContains(t, err, expectedErrMsg)
|
||||
})
|
||||
|
||||
t.Run("does not return error when plugin is already updated", func(t *testing.T) {
|
||||
// update plugin twice to test already updated case
|
||||
updatedToRef, err := plugin.Update("")
|
||||
assert.Nil(t, err)
|
||||
updatedToRef2, err := plugin.Update("")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, updatedToRef, updatedToRef2)
|
||||
})
|
||||
|
||||
t.Run("updates plugin when plugin when plugin exists", func(t *testing.T) {
|
||||
latestHash, err := getCurrentCommit(directory)
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, err = checkoutPreviousCommit(directory)
|
||||
assert.Nil(t, err)
|
||||
|
||||
updatedToRef, err := plugin.Update("")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, latestHash, updatedToRef)
|
||||
|
||||
currentHash, err := getCurrentCommit(directory)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, latestHash, currentHash)
|
||||
})
|
||||
|
||||
t.Run("Returns error when specified ref does not exist", func(t *testing.T) {
|
||||
ref := "non-existant"
|
||||
updatedToRef, err := plugin.Update(ref)
|
||||
assert.Equal(t, updatedToRef, "")
|
||||
expectedErrMsg := "couldn't find remote ref \"non-existant\""
|
||||
assert.ErrorContains(t, err, expectedErrMsg)
|
||||
|
||||
})
|
||||
|
||||
t.Run("updates plugin to ref when plugin with name and ref exist", func(t *testing.T) {
|
||||
ref := "master"
|
||||
|
||||
hash, err := getCommit(directory, ref)
|
||||
assert.Nil(t, err)
|
||||
|
||||
updatedToRef, err := plugin.Update(ref)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, hash, updatedToRef)
|
||||
|
||||
// Check that plugin was updated to ref
|
||||
latestHash, err := getCurrentCommit(directory)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, hash, latestHash)
|
||||
})
|
||||
}
|
||||
|
||||
func getCurrentCommit(path string) (string, error) {
|
||||
return getCommit(path, "HEAD")
|
||||
}
|
||||
|
||||
func getCommit(path, revision string) (string, error) {
|
||||
repo, err := git.PlainOpen(path)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash, err := repo.ResolveRevision(plumbing.Revision(revision))
|
||||
|
||||
return hash.String(), err
|
||||
}
|
||||
|
||||
func checkoutPreviousCommit(path string) (string, error) {
|
||||
repo, err := git.PlainOpen(path)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
previousHash, err := repo.ResolveRevision(plumbing.Revision("HEAD~"))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = worktree.Reset(&git.ResetOptions{Commit: *previousHash})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return previousHash.String(), nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user