diff --git a/pkg/cmd/extensions/extension.go b/pkg/cmd/extensions/extension.go index 33b2f8d67..8fa07e7cf 100644 --- a/pkg/cmd/extensions/extension.go +++ b/pkg/cmd/extensions/extension.go @@ -1,6 +1,8 @@ package extensions import ( + "errors" + "os" "path/filepath" "strings" ) @@ -21,3 +23,20 @@ func (e *Extension) Path() string { func (e *Extension) URL() string { return e.url } + +func (e *Extension) IsLocal() bool { + dir := filepath.Dir(e.path) + fileInfo, err := os.Lstat(dir) + if err != nil { + return false + } + // Check if extension is a symlink + if fileInfo.Mode()&os.ModeSymlink != 0 { + return true + } + // Check if extension does not have a git directory + if _, err = os.Stat(filepath.Join(dir, ".git")); errors.Is(err, os.ErrNotExist) { + return true + } + return false +} diff --git a/pkg/cmd/extensions/manager.go b/pkg/cmd/extensions/manager.go index 09520a930..ff6452657 100644 --- a/pkg/cmd/extensions/manager.go +++ b/pkg/cmd/extensions/manager.go @@ -136,6 +136,8 @@ func (m *Manager) Install(cloneURL string, stdout, stderr io.Writer) error { return externalCmd.Run() } +var localExtensionUpgradeError = errors.New("local extensions can not be upgraded") + func (m *Manager) Upgrade(name string, stdout, stderr io.Writer) error { exe, err := m.lookPath("git") if err != nil { @@ -154,6 +156,16 @@ func (m *Manager) Upgrade(name string, stdout, stderr io.Writer) error { } else if f.Name() != name { continue } + + if f.IsLocal() { + if name == "" { + fmt.Fprintf(stdout, "%s\n", localExtensionUpgradeError) + } else { + err = localExtensionUpgradeError + } + continue + } + dir := filepath.Dir(f.Path()) externalCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only") externalCmd.Stdout = stdout diff --git a/pkg/cmd/extensions/manager_test.go b/pkg/cmd/extensions/manager_test.go index d829b156a..693bfed89 100644 --- a/pkg/cmd/extensions/manager_test.go +++ b/pkg/cmd/extensions/manager_test.go @@ -44,8 +44,8 @@ func newTestManager(dir string) *Manager { func TestManager_List(t *testing.T) { tempDir := t.TempDir() - assert.NoError(t, stubExecutable(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) - assert.NoError(t, stubExecutable(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) m := newTestManager(tempDir) exts := m.List() @@ -57,7 +57,7 @@ func TestManager_List(t *testing.T) { func TestManager_Dispatch(t *testing.T) { tempDir := t.TempDir() extPath := filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello") - assert.NoError(t, stubExecutable(extPath)) + assert.NoError(t, stubExtension(extPath)) m := newTestManager(tempDir) @@ -77,8 +77,8 @@ func TestManager_Dispatch(t *testing.T) { func TestManager_Remove(t *testing.T) { tempDir := t.TempDir() - assert.NoError(t, stubExecutable(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) - assert.NoError(t, stubExecutable(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) m := newTestManager(tempDir) err := m.Remove("hello") @@ -90,10 +90,11 @@ func TestManager_Remove(t *testing.T) { assert.Equal(t, "gh-two", items[0].Name()) } -func TestManager_Upgrade(t *testing.T) { +func TestManager_Upgrade_AllExtensions(t *testing.T) { tempDir := t.TempDir() - assert.NoError(t, stubExecutable(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) - assert.NoError(t, stubExecutable(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello"))) + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-two", "gh-two"))) + assert.NoError(t, stubLocalExtension(filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) m := newTestManager(tempDir) @@ -105,6 +106,7 @@ func TestManager_Upgrade(t *testing.T) { assert.Equal(t, heredoc.Docf( ` [hello]: [git -C %s --git-dir=%s pull --ff-only] + [local]: local extensions can not be upgraded [two]: [git -C %s --git-dir=%s pull --ff-only] `, filepath.Join(tempDir, "extensions", "gh-hello"), @@ -115,6 +117,53 @@ func TestManager_Upgrade(t *testing.T) { assert.Equal(t, "", stderr.String()) } +func TestManager_Upgrade_RemoteExtension(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote"))) + + m := newTestManager(tempDir) + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + err := m.Upgrade("remote", stdout, stderr) + assert.NoError(t, err) + assert.Equal(t, heredoc.Docf( + ` + [git -C %s --git-dir=%s pull --ff-only] + `, + filepath.Join(tempDir, "extensions", "gh-remote"), + filepath.Join(tempDir, "extensions", "gh-remote", ".git"), + ), stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_Upgrade_LocalExtension(t *testing.T) { + tempDir := t.TempDir() + assert.NoError(t, stubLocalExtension(filepath.Join(tempDir, "extensions", "gh-local", "gh-local"))) + + m := newTestManager(tempDir) + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + err := m.Upgrade("local", stdout, stderr) + assert.EqualError(t, err, "local extensions can not be upgraded") + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + +func TestManager_Upgrade_NoExtensions(t *testing.T) { + tempDir := t.TempDir() + + m := newTestManager(tempDir) + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + err := m.Upgrade("", stdout, stderr) + assert.EqualError(t, err, "no extensions installed") + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + func TestManager_Install(t *testing.T) { tempDir := t.TempDir() m := newTestManager(tempDir) @@ -127,7 +176,23 @@ func TestManager_Install(t *testing.T) { assert.Equal(t, "", stderr.String()) } -func stubExecutable(path string) error { +func stubExtension(path string) error { + dir := filepath.Dir(path) + gitDir := filepath.Join(dir, ".git") + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + if err := os.Mkdir(gitDir, 0755); err != nil { + return err + } + f, err := os.OpenFile(path, os.O_CREATE, 0755) + if err != nil { + return err + } + return f.Close() +} + +func stubLocalExtension(path string) error { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go index c0d65e297..da915463a 100644 --- a/pkg/extensions/extension.go +++ b/pkg/extensions/extension.go @@ -9,6 +9,7 @@ type Extension interface { Name() string Path() string URL() string + IsLocal() bool } //go:generate moq -out manager_mock.go . ExtensionManager diff --git a/pkg/extensions/extension_mock.go b/pkg/extensions/extension_mock.go index fb32db5b1..8fe35c1f6 100644 --- a/pkg/extensions/extension_mock.go +++ b/pkg/extensions/extension_mock.go @@ -17,6 +17,9 @@ var _ Extension = &ExtensionMock{} // // // make and configure a mocked Extension // mockedExtension := &ExtensionMock{ +// IsLocalFunc: func() bool { +// panic("mock out the IsLocal method") +// }, // NameFunc: func() string { // panic("mock out the Name method") // }, @@ -33,6 +36,9 @@ var _ Extension = &ExtensionMock{} // // } type ExtensionMock struct { + // IsLocalFunc mocks the IsLocal method. + IsLocalFunc func() bool + // NameFunc mocks the Name method. NameFunc func() string @@ -44,6 +50,9 @@ type ExtensionMock struct { // calls tracks calls to the methods. calls struct { + // IsLocal holds details about calls to the IsLocal method. + IsLocal []struct { + } // Name holds details about calls to the Name method. Name []struct { } @@ -54,9 +63,36 @@ type ExtensionMock struct { URL []struct { } } - lockName sync.RWMutex - lockPath sync.RWMutex - lockURL sync.RWMutex + lockIsLocal sync.RWMutex + lockName sync.RWMutex + lockPath sync.RWMutex + lockURL sync.RWMutex +} + +// IsLocal calls IsLocalFunc. +func (mock *ExtensionMock) IsLocal() bool { + if mock.IsLocalFunc == nil { + panic("ExtensionMock.IsLocalFunc: method is nil but Extension.IsLocal was just called") + } + callInfo := struct { + }{} + mock.lockIsLocal.Lock() + mock.calls.IsLocal = append(mock.calls.IsLocal, callInfo) + mock.lockIsLocal.Unlock() + return mock.IsLocalFunc() +} + +// IsLocalCalls gets all the calls that were made to IsLocal. +// Check the length with: +// len(mockedExtension.IsLocalCalls()) +func (mock *ExtensionMock) IsLocalCalls() []struct { +} { + var calls []struct { + } + mock.lockIsLocal.RLock() + calls = mock.calls.IsLocal + mock.lockIsLocal.RUnlock() + return calls } // Name calls NameFunc.