Skip to content

Commit 325cd33

Browse files
committed
feat(golang-rewrite): create plugins/git package to store plugin Git operations
1 parent 3ffeec2 commit 325cd33

File tree

2 files changed

+353
-0
lines changed

2 files changed

+353
-0
lines changed

plugins/git/git.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Package git contains all the Git operations that can be applied to asdf
2+
// plugins
3+
package git
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/go-git/go-git/v5"
9+
"github.com/go-git/go-git/v5/config"
10+
"github.com/go-git/go-git/v5/plumbing"
11+
)
12+
13+
const remoteName = "origin"
14+
15+
// Plugin is a struct to contain the plugin Git details
16+
type Plugin struct {
17+
directory string
18+
}
19+
20+
// PluginOps is an interface for operations that can be applied to asdf plugins.
21+
// Right now we only support Git, but in the future we might have other
22+
// mechanisms to install and upgrade plugins. asdf doesn't require a plugin
23+
// to be a Git repository when asdf uses it, but Git is the only way to install
24+
// and upgrade plugins. If other approaches are supported this will be
25+
// extracted into the `plugins` module.
26+
type PluginOps interface {
27+
Clone(pluginURL string) error
28+
Head() (string, error)
29+
RemoteURL() (string, error)
30+
Update(ref string) (string, error)
31+
}
32+
33+
// NewPlugin builds a new Plugin instance
34+
func NewPlugin(directory string) Plugin {
35+
return Plugin{directory: directory}
36+
}
37+
38+
// Clone installs a plugin via Git
39+
func (g Plugin) Clone(pluginURL string) error {
40+
_, err := git.PlainClone(g.directory, false, &git.CloneOptions{
41+
URL: pluginURL,
42+
})
43+
44+
if err != nil {
45+
return fmt.Errorf("unable to clone plugin: %w", err)
46+
}
47+
48+
return nil
49+
}
50+
51+
// Head returns the current HEAD ref of the plugin's Git repository
52+
func (g Plugin) Head() (string, error) {
53+
repo, err := gitOpen(g.directory)
54+
55+
if err != nil {
56+
return "", err
57+
}
58+
59+
ref, err := repo.Head()
60+
if err != nil {
61+
return "", err
62+
}
63+
64+
return ref.Hash().String(), nil
65+
}
66+
67+
// RemoteURL returns the URL of the default remote for the plugin's Git repository
68+
func (g Plugin) RemoteURL() (string, error) {
69+
repo, err := gitOpen(g.directory)
70+
71+
if err != nil {
72+
return "", err
73+
}
74+
75+
remotes, err := repo.Remotes()
76+
if err != nil {
77+
return "", err
78+
}
79+
80+
return remotes[0].Config().URLs[0], nil
81+
}
82+
83+
// Update updates the plugin's Git repository to the ref if provided, or the
84+
// latest commit on the current branch
85+
func (g Plugin) Update(ref string) (string, error) {
86+
repo, err := gitOpen(g.directory)
87+
88+
if err != nil {
89+
return "", err
90+
}
91+
92+
var checkoutOptions git.CheckoutOptions
93+
94+
if ref == "" {
95+
// If no ref is provided checkout latest commit on current branch
96+
head, err := repo.Head()
97+
98+
if err != nil {
99+
return "", err
100+
}
101+
102+
if !head.Name().IsBranch() {
103+
return "", fmt.Errorf("not on a branch, unable to update")
104+
}
105+
106+
// If on a branch checkout the latest version of it from the remote
107+
branch := head.Name()
108+
ref = branch.String()
109+
checkoutOptions = git.CheckoutOptions{Branch: branch, Force: true}
110+
} else {
111+
// Checkout ref if provided
112+
checkoutOptions = git.CheckoutOptions{Hash: plumbing.NewHash(ref), Force: true}
113+
}
114+
115+
fetchOptions := git.FetchOptions{RemoteName: remoteName, Force: true, RefSpecs: []config.RefSpec{
116+
config.RefSpec(ref + ":" + ref),
117+
}}
118+
119+
err = repo.Fetch(&fetchOptions)
120+
121+
if err != nil && err != git.NoErrAlreadyUpToDate {
122+
return "", err
123+
}
124+
125+
worktree, err := repo.Worktree()
126+
if err != nil {
127+
return "", err
128+
}
129+
130+
err = worktree.Checkout(&checkoutOptions)
131+
if err != nil {
132+
return "", err
133+
}
134+
135+
hash, err := repo.ResolveRevision(plumbing.Revision("HEAD"))
136+
return hash.String(), err
137+
}
138+
139+
func gitOpen(directory string) (*git.Repository, error) {
140+
repo, err := git.PlainOpen(directory)
141+
142+
if err != nil {
143+
return repo, fmt.Errorf("unable to open plugin Git repository: %w", err)
144+
}
145+
146+
return repo, nil
147+
}

plugins/git/git_test.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package git
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/go-git/go-git/v5"
9+
"github.com/go-git/go-git/v5/plumbing"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
// TODO: Switch to local repo so tests don't go over the network
14+
const (
15+
testRepo = "https://github.com/Stratus3D/asdf-lua"
16+
testPluginName = "lua"
17+
)
18+
19+
func TestPluginClone(t *testing.T) {
20+
t.Run("when plugin name is valid but URL is invalid prints an error", func(t *testing.T) {
21+
tempDir := t.TempDir()
22+
directory := filepath.Join(tempDir, testPluginName)
23+
24+
plugin := NewPlugin(directory)
25+
err := plugin.Clone("foobar")
26+
27+
assert.ErrorContains(t, err, "unable to clone plugin: repository not found")
28+
})
29+
30+
t.Run("clones provided Git URL to plugin directory when URL is valid", func(t *testing.T) {
31+
tempDir := t.TempDir()
32+
directory := filepath.Join(tempDir, testPluginName)
33+
34+
plugin := NewPlugin(directory)
35+
err := plugin.Clone(testRepo)
36+
37+
assert.Nil(t, err)
38+
39+
// Assert plugin directory contains Git repo with bin directory
40+
_, err = os.ReadDir(directory + "/.git")
41+
assert.Nil(t, err)
42+
43+
entries, err := os.ReadDir(directory + "/bin")
44+
assert.Nil(t, err)
45+
assert.Equal(t, 5, len(entries))
46+
})
47+
}
48+
49+
func TestPluginHead(t *testing.T) {
50+
tempDir := t.TempDir()
51+
directory := filepath.Join(tempDir, testPluginName)
52+
53+
plugin := NewPlugin(directory)
54+
55+
err := plugin.Clone(testRepo)
56+
assert.Nil(t, err)
57+
58+
head, err := plugin.Head()
59+
assert.Nil(t, err)
60+
assert.NotZero(t, head)
61+
}
62+
63+
func TestPluginRemoteURL(t *testing.T) {
64+
tempDir := t.TempDir()
65+
directory := filepath.Join(tempDir, testPluginName)
66+
67+
plugin := NewPlugin(directory)
68+
69+
err := plugin.Clone(testRepo)
70+
assert.Nil(t, err)
71+
72+
url, err := plugin.RemoteURL()
73+
assert.Nil(t, err)
74+
assert.NotZero(t, url)
75+
}
76+
77+
func TestPluginUpdate(t *testing.T) {
78+
tempDir := t.TempDir()
79+
directory := filepath.Join(tempDir, testPluginName)
80+
81+
plugin := NewPlugin(directory)
82+
83+
err := plugin.Clone(testRepo)
84+
assert.Nil(t, err)
85+
86+
t.Run("returns error when plugin with name does not exist", func(t *testing.T) {
87+
nonexistantPath := filepath.Join(tempDir, "nonexistant")
88+
nonexistantPlugin := NewPlugin(nonexistantPath)
89+
updatedToRef, err := nonexistantPlugin.Update("")
90+
91+
assert.NotNil(t, err)
92+
assert.Equal(t, updatedToRef, "")
93+
expectedErrMsg := "unable to open plugin Git repository: repository does not exist"
94+
assert.ErrorContains(t, err, expectedErrMsg)
95+
})
96+
97+
t.Run("returns error when plugin repo does not exist", func(t *testing.T) {
98+
badPluginName := "badplugin"
99+
badPluginDir := filepath.Join(tempDir, badPluginName)
100+
err := os.MkdirAll(badPluginDir, 0777)
101+
assert.Nil(t, err)
102+
103+
badPlugin := NewPlugin(badPluginDir)
104+
105+
updatedToRef, err := badPlugin.Update("")
106+
107+
assert.NotNil(t, err)
108+
assert.Equal(t, updatedToRef, "")
109+
expectedErrMsg := "unable to open plugin Git repository: repository does not exist"
110+
assert.ErrorContains(t, err, expectedErrMsg)
111+
})
112+
113+
t.Run("does not return error when plugin is already updated", func(t *testing.T) {
114+
// update plugin twice to test already updated case
115+
updatedToRef, err := plugin.Update("")
116+
assert.Nil(t, err)
117+
updatedToRef2, err := plugin.Update("")
118+
assert.Nil(t, err)
119+
assert.Equal(t, updatedToRef, updatedToRef2)
120+
})
121+
122+
t.Run("updates plugin when plugin when plugin exists", func(t *testing.T) {
123+
latestHash, err := getCurrentCommit(directory)
124+
assert.Nil(t, err)
125+
126+
_, err = checkoutPreviousCommit(directory)
127+
assert.Nil(t, err)
128+
129+
updatedToRef, err := plugin.Update("")
130+
assert.Nil(t, err)
131+
assert.Equal(t, latestHash, updatedToRef)
132+
133+
currentHash, err := getCurrentCommit(directory)
134+
assert.Nil(t, err)
135+
assert.Equal(t, latestHash, currentHash)
136+
})
137+
138+
t.Run("Returns error when specified ref does not exist", func(t *testing.T) {
139+
ref := "non-existant"
140+
updatedToRef, err := plugin.Update(ref)
141+
assert.Equal(t, updatedToRef, "")
142+
expectedErrMsg := "couldn't find remote ref \"non-existant\""
143+
assert.ErrorContains(t, err, expectedErrMsg)
144+
145+
})
146+
147+
t.Run("updates plugin to ref when plugin with name and ref exist", func(t *testing.T) {
148+
ref := "master"
149+
150+
hash, err := getCommit(directory, ref)
151+
assert.Nil(t, err)
152+
153+
updatedToRef, err := plugin.Update(ref)
154+
assert.Nil(t, err)
155+
assert.Equal(t, hash, updatedToRef)
156+
157+
// Check that plugin was updated to ref
158+
latestHash, err := getCurrentCommit(directory)
159+
assert.Nil(t, err)
160+
assert.Equal(t, hash, latestHash)
161+
})
162+
}
163+
164+
func getCurrentCommit(path string) (string, error) {
165+
return getCommit(path, "HEAD")
166+
}
167+
168+
func getCommit(path, revision string) (string, error) {
169+
repo, err := git.PlainOpen(path)
170+
171+
if err != nil {
172+
return "", err
173+
}
174+
175+
hash, err := repo.ResolveRevision(plumbing.Revision(revision))
176+
177+
return hash.String(), err
178+
}
179+
180+
func checkoutPreviousCommit(path string) (string, error) {
181+
repo, err := git.PlainOpen(path)
182+
183+
if err != nil {
184+
return "", err
185+
}
186+
187+
previousHash, err := repo.ResolveRevision(plumbing.Revision("HEAD~"))
188+
189+
if err != nil {
190+
return "", err
191+
}
192+
193+
worktree, err := repo.Worktree()
194+
195+
if err != nil {
196+
return "", err
197+
}
198+
199+
err = worktree.Reset(&git.ResetOptions{Commit: *previousHash})
200+
201+
if err != nil {
202+
return "", err
203+
}
204+
205+
return previousHash.String(), nil
206+
}

0 commit comments

Comments
 (0)