Use git client in extension manager (#6547)

This commit is contained in:
Sam Coe 2022-11-10 11:38:12 +02:00 committed by GitHub
parent 30ad3f0fd1
commit 6bbfc5056d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 381 additions and 161 deletions

View file

@ -32,7 +32,7 @@ type Client struct {
mu sync.Mutex
}
func (c *Client) Command(ctx context.Context, args ...string) (*gitCommand, error) {
func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) {
if c.RepoDir != "" {
args = append([]string{"-C", c.RepoDir}, args...)
}
@ -53,12 +53,12 @@ func (c *Client) Command(ctx context.Context, args ...string) (*gitCommand, erro
cmd.Stderr = c.Stderr
cmd.Stdin = c.Stdin
cmd.Stdout = c.Stdout
return &gitCommand{cmd}, nil
return &Command{cmd}, nil
}
// AuthenticatedCommand is a wrapper around Command that included configuration to use gh
// as the credential helper for git.
func (c *Client) AuthenticatedCommand(ctx context.Context, args ...string) (*gitCommand, error) {
func (c *Client) AuthenticatedCommand(ctx context.Context, args ...string) (*Command, error) {
preArgs := []string{"-c", "credential.helper="}
if c.GhPath == "" {
// Assumes that gh is in PATH.
@ -414,7 +414,10 @@ func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods
}
func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...CommandModifier) error {
args := []string{"pull", "--ff-only", remote, branch}
args := []string{"pull", "--ff-only"}
if remote != "" && branch != "" {
args = append(args, remote, branch)
}
// TODO: Use AuthenticatedCommand
cmd, err := c.Command(ctx, args...)
if err != nil {

View file

@ -12,11 +12,11 @@ import (
type commandCtx = func(ctx context.Context, name string, args ...string) *exec.Cmd
type gitCommand struct {
type Command struct {
*exec.Cmd
}
func (gc *gitCommand) Run() error {
func (gc *Command) Run() error {
stderr := &bytes.Buffer{}
if gc.Cmd.Stderr == nil {
gc.Cmd.Stderr = stderr
@ -35,7 +35,7 @@ func (gc *gitCommand) Run() error {
return nil
}
func (gc *gitCommand) Output() ([]byte, error) {
func (gc *Command) Output() ([]byte, error) {
gc.Stdout = nil
gc.Stderr = nil
// This is a hack in order to not break the hundreds of
@ -53,7 +53,7 @@ func (gc *gitCommand) Output() ([]byte, error) {
return out, err
}
func (gc *gitCommand) setRepoDir(repoDir string) {
func (gc *Command) setRepoDir(repoDir string) {
for i, arg := range gc.Args {
if arg == "-C" {
gc.Args[i+1] = repoDir
@ -73,28 +73,28 @@ func (gc *gitCommand) setRepoDir(repoDir string) {
}
// Allow individual commands to be modified from the default client options.
type CommandModifier func(*gitCommand)
type CommandModifier func(*Command)
func WithStderr(stderr io.Writer) CommandModifier {
return func(gc *gitCommand) {
return func(gc *Command) {
gc.Stderr = stderr
}
}
func WithStdout(stdout io.Writer) CommandModifier {
return func(gc *gitCommand) {
return func(gc *Command) {
gc.Stdout = stdout
}
}
func WithStdin(stdin io.Reader) CommandModifier {
return func(gc *gitCommand) {
return func(gc *Command) {
gc.Stdin = stdin
}
}
func WithRepoDir(repoDir string) CommandModifier {
return func(gc *gitCommand) {
return func(gc *Command) {
gc.setRepoDir(repoDir)
}
}

67
pkg/cmd/extension/git.go Normal file
View file

@ -0,0 +1,67 @@
package extension
import (
"context"
"github.com/cli/cli/v2/git"
)
type gitClient interface {
CheckoutBranch(branch string) error
Clone(cloneURL string, args []string) (string, error)
CommandOutput(args []string) ([]byte, error)
Config(name string) (string, error)
Fetch(remote string, refspec string) error
ForRepo(repoDir string) gitClient
Pull(remote, branch string) error
Remotes() (git.RemoteSet, error)
}
type gitExecuter struct {
client *git.Client
}
func (g *gitExecuter) CheckoutBranch(branch string) error {
return g.client.CheckoutBranch(context.Background(), branch)
}
func (g *gitExecuter) Clone(cloneURL string, cloneArgs []string) (string, error) {
return g.client.Clone(context.Background(), cloneURL, cloneArgs)
}
func (g *gitExecuter) CommandOutput(args []string) ([]byte, error) {
cmd, err := g.client.Command(context.Background(), args...)
if err != nil {
return nil, err
}
return cmd.Output()
}
func (g *gitExecuter) Config(name string) (string, error) {
return g.client.Config(context.Background(), name)
}
func (g *gitExecuter) Fetch(remote string, refspec string) error {
return g.client.Fetch(context.Background(), remote, refspec)
}
func (g *gitExecuter) ForRepo(repoDir string) gitClient {
return &gitExecuter{
client: &git.Client{
GhPath: g.client.GhPath,
RepoDir: repoDir,
GitPath: g.client.GitPath,
Stderr: g.client.Stderr,
Stdin: g.client.Stdin,
Stdout: g.client.Stdout,
},
}
}
func (g *gitExecuter) Pull(remote, branch string) error {
return g.client.Pull(context.Background(), remote, branch)
}
func (g *gitExecuter) Remotes() (git.RemoteSet, error) {
return g.client.Remotes(context.Background())
}

View file

@ -2,7 +2,6 @@ package extension
import (
"bytes"
"context"
_ "embed"
"errors"
"fmt"
@ -35,12 +34,13 @@ type Manager struct {
newCommand func(string, ...string) *exec.Cmd
platform func() (string, string)
client *http.Client
gitClient gitClient
config config.Config
io *iostreams.IOStreams
dryRunMode bool
}
func NewManager(ios *iostreams.IOStreams) *Manager {
func NewManager(ios *iostreams.IOStreams, gc *git.Client) *Manager {
return &Manager{
dataDir: config.DataDir,
lookPath: safeexec.LookPath,
@ -53,7 +53,8 @@ func NewManager(ios *iostreams.IOStreams) *Manager {
}
return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), ext
},
io: ios,
io: ios,
gitClient: &gitExecuter{client: gc},
}
}
@ -231,15 +232,9 @@ func (m *Manager) parseGitExtensionDir(fi fs.DirEntry) (Extension, error) {
// getCurrentVersion determines the current version for non-local git extensions.
func (m *Manager) getCurrentVersion(extension string) string {
gitExe, err := m.lookPath("git")
if err != nil {
return ""
}
dir := m.installDir()
gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git")
cmd := m.newCommand(gitExe, gitDir, "rev-parse", "HEAD")
localSha, err := cmd.Output()
dir := filepath.Join(m.installDir(), extension)
scopedClient := m.gitClient.ForRepo(dir)
localSha, err := scopedClient.CommandOutput([]string{"rev-parse", "HEAD"})
if err != nil {
return ""
}
@ -248,14 +243,9 @@ func (m *Manager) getCurrentVersion(extension string) string {
// getRemoteUrl determines the remote URL for non-local git extensions.
func (m *Manager) getRemoteUrl(extension string) string {
gitExe, err := m.lookPath("git")
if err != nil {
return ""
}
dir := m.installDir()
gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git")
cmd := m.newCommand(gitExe, gitDir, "config", "remote.origin.url")
url, err := cmd.Output()
dir := filepath.Join(m.installDir(), extension)
scopedClient := m.gitClient.ForRepo(dir)
url, err := scopedClient.Config("remote.origin.url")
if err != nil {
return ""
}
@ -301,14 +291,9 @@ func (m *Manager) getLatestVersion(ext Extension) (string, error) {
}
return r.Tag, nil
} else {
gitExe, err := m.lookPath("git")
if err != nil {
return "", err
}
extDir := filepath.Dir(ext.path)
gitDir := "--git-dir=" + filepath.Join(extDir, ".git")
cmd := m.newCommand(gitExe, gitDir, "ls-remote", "origin", "HEAD")
lsRemote, err := cmd.Output()
scopedClient := m.gitClient.ForRepo(extDir)
lsRemote, err := scopedClient.CommandOutput([]string{"ls-remote", "origin", "HEAD"})
if err != nil {
return "", err
}
@ -469,13 +454,9 @@ func (m *Manager) installGit(repo ghrepo.Interface, target string, stdout, stder
protocol, _ := m.config.GetOrDefault(repo.RepoHost(), "git_protocol")
cloneURL := ghrepo.FormatRemoteURL(repo, protocol)
exe, err := m.lookPath("git")
if err != nil {
return err
}
var commitSHA string
if target != "" {
var err error
commitSHA, err = fetchCommitSHA(m.client, repo, target)
if err != nil {
return err
@ -485,20 +466,17 @@ func (m *Manager) installGit(repo ghrepo.Interface, target string, stdout, stder
name := strings.TrimSuffix(path.Base(cloneURL), ".git")
targetDir := filepath.Join(m.installDir(), name)
externalCmd := m.newCommand(exe, "clone", cloneURL, targetDir)
externalCmd.Stdout = stdout
externalCmd.Stderr = stderr
if err := externalCmd.Run(); err != nil {
_, err := m.gitClient.Clone(cloneURL, []string{targetDir})
if err != nil {
return err
}
if commitSHA == "" {
return nil
}
checkoutCmd := m.newCommand(exe, "-C", targetDir, "checkout", commitSHA)
checkoutCmd.Stdout = stdout
checkoutCmd.Stderr = stderr
if err := checkoutCmd.Run(); err != nil {
scopedClient := m.gitClient.ForRepo(targetDir)
err = scopedClient.CheckoutBranch(commitSHA)
if err != nil {
return err
}
@ -587,7 +565,7 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
} else {
// Check if git extension has changed to a binary extension
var isBin bool
repo, repoErr := repoFromPath(filepath.Join(ext.Path(), ".."))
repo, repoErr := repoFromPath(m.gitClient, filepath.Join(ext.Path(), ".."))
if repoErr == nil {
isBin, _ = isBinExtension(m.client, repo)
}
@ -603,21 +581,22 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
}
func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
exe, err := m.lookPath("git")
if err != nil {
return err
}
dir := filepath.Dir(ext.path)
if m.dryRunMode {
return nil
}
dir := filepath.Dir(ext.path)
scopedClient := m.gitClient.ForRepo(dir)
if force {
if err := m.newCommand(exe, "-C", dir, "fetch", "origin", "HEAD").Run(); err != nil {
err := scopedClient.Fetch("origin", "HEAD")
if err != nil {
return err
}
return m.newCommand(exe, "-C", dir, "reset", "--hard", "origin/HEAD").Run()
_, err = scopedClient.CommandOutput([]string{"reset", "--hard", "origin/HEAD"})
return err
}
return m.newCommand(exe, "-C", dir, "pull", "--ff-only").Run()
return scopedClient.Pull("", "")
}
func (m *Manager) upgradeBinExtension(ext Extension) error {
@ -659,19 +638,14 @@ var scriptTmpl string
var buildScript []byte
func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error {
exe, err := m.lookPath("git")
if err != nil {
return err
}
if err := m.newCommand(exe, "init", "--quiet", name).Run(); err != nil {
if _, err := m.gitClient.CommandOutput([]string{"init", "--quiet", name}); err != nil {
return err
}
if tmplType == extensions.GoBinTemplateType {
return m.goBinScaffolding(exe, name)
return m.goBinScaffolding(name)
} else if tmplType == extensions.OtherBinTemplateType {
return m.otherBinScaffolding(exe, name)
return m.otherBinScaffolding(name)
}
script := fmt.Sprintf(scriptTmpl, name)
@ -679,10 +653,12 @@ func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error
return err
}
return m.newCommand(exe, "-C", name, "add", name, "--chmod=+x").Run()
scopedClient := m.gitClient.ForRepo(name)
_, err := scopedClient.CommandOutput([]string{"add", name, "--chmod=+x"})
return err
}
func (m *Manager) otherBinScaffolding(gitExe, name string) error {
func (m *Manager) otherBinScaffolding(name string) error {
if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), otherBinWorkflow, 0644); err != nil {
return err
}
@ -690,13 +666,17 @@ func (m *Manager) otherBinScaffolding(gitExe, name string) error {
if err := writeFile(filepath.Join(name, buildScriptPath), buildScript, 0755); err != nil {
return err
}
if err := m.newCommand(gitExe, "-C", name, "add", buildScriptPath, "--chmod=+x").Run(); err != nil {
scopedClient := m.gitClient.ForRepo(name)
if _, err := scopedClient.CommandOutput([]string{"add", buildScriptPath, "--chmod=+x"}); err != nil {
return err
}
return m.newCommand(gitExe, "-C", name, "add", ".").Run()
_, err := scopedClient.CommandOutput([]string{"add", "."})
return err
}
func (m *Manager) goBinScaffolding(gitExe, name string) error {
func (m *Manager) goBinScaffolding(name string) error {
goExe, err := m.lookPath("go")
if err != nil {
return fmt.Errorf("go is required for creating Go extensions: %w", err)
@ -737,7 +717,9 @@ func (m *Manager) goBinScaffolding(gitExe, name string) error {
}
}
return m.newCommand(gitExe, "-C", name, "add", ".").Run()
scopedClient := m.gitClient.ForRepo(name)
_, err = scopedClient.CommandOutput([]string{"add", "."})
return err
}
func isSymlink(m os.FileMode) bool {
@ -789,9 +771,9 @@ func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err
return
}
func repoFromPath(path string) (ghrepo.Interface, error) {
gitClient := &git.Client{RepoDir: path}
remotes, err := gitClient.Remotes(context.Background())
func repoFromPath(gitClient gitClient, path string) (ghrepo.Interface, error) {
scopedClient := gitClient.ForRepo(path)
remotes, err := scopedClient.Remotes()
if err != nil {
return nil, err
}

View file

@ -9,10 +9,10 @@ import (
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/run"
@ -29,15 +29,6 @@ func TestHelperProcess(t *testing.T) {
return
}
if err := func(args []string) error {
// git init should create the directory named by argument
if len(args) > 2 && strings.HasPrefix(strings.Join(args, " "), "git init") {
dir := args[len(args)-1]
if !strings.HasPrefix(dir, "-") {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
}
fmt.Fprintf(os.Stdout, "%v\n", args)
return nil
}(os.Args[3:]); err != nil {
@ -47,7 +38,7 @@ func TestHelperProcess(t *testing.T) {
os.Exit(0)
}
func newTestManager(dir string, client *http.Client, ios *iostreams.IOStreams) *Manager {
func newTestManager(dir string, client *http.Client, gitClient gitClient, ios *iostreams.IOStreams) *Manager {
return &Manager{
dataDir: func() string { return dir },
lookPath: func(exe string) (string, error) { return exe, nil },
@ -62,9 +53,10 @@ func newTestManager(dir string, client *http.Client, ios *iostreams.IOStreams) *
cmd.Env = []string{"GH_WANT_HELPER_PROCESS=1"}
return cmd
},
config: config.NewBlankConfig(),
io: ios,
client: client,
config: config.NewBlankConfig(),
io: ios,
client: client,
gitClient: gitClient,
platform: func() (string, string) {
return "windows-amd64", ".exe"
},
@ -85,12 +77,26 @@ func TestManager_List(t *testing.T) {
Tag: "v1.0.1",
}))
m := newTestManager(tempDir, nil, nil)
dirOne := filepath.Join(tempDir, "extensions", "gh-hello")
dirTwo := filepath.Join(tempDir, "extensions", "gh-two")
gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", dirOne).Return(gcOne).Twice()
gc.On("ForRepo", dirTwo).Return(gcTwo).Twice()
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcTwo.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcTwo.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
m := newTestManager(tempDir, nil, gc, nil)
exts := m.List()
assert.Equal(t, 3, len(exts))
assert.Equal(t, "bin-ext", exts[0].Name())
assert.Equal(t, "hello", exts[1].Name())
assert.Equal(t, "two", exts[2].Name())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
gcTwo.AssertExpectations(t)
}
func TestManager_list_includeMetadata(t *testing.T) {
@ -122,7 +128,7 @@ func TestManager_list_includeMetadata(t *testing.T) {
},
}))
m := newTestManager(tempDir, &client, nil)
m := newTestManager(tempDir, &client, nil, nil)
exts, err := m.list(true)
assert.NoError(t, err)
@ -134,10 +140,16 @@ func TestManager_list_includeMetadata(t *testing.T) {
func TestManager_Dispatch(t *testing.T) {
tempDir := t.TempDir()
extDir := filepath.Join(tempDir, "extensions", "gh-hello")
extPath := filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")
assert.NoError(t, stubExtension(extPath))
m := newTestManager(tempDir, nil, nil)
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", extDir).Return(gcOne).Twice()
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
m := newTestManager(tempDir, nil, gc, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -151,6 +163,9 @@ func TestManager_Dispatch(t *testing.T) {
assert.Equal(t, fmt.Sprintf("[%s one two]\n", extPath), stdout.String())
}
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_Dispatch_binary(t *testing.T) {
@ -165,7 +180,7 @@ func TestManager_Dispatch_binary(t *testing.T) {
}
assert.NoError(t, stubBinaryExtension(extPath, bm))
m := newTestManager(tempDir, nil, nil)
m := newTestManager(tempDir, nil, nil, nil)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
@ -182,7 +197,7 @@ func TestManager_Remove(t *testing.T) {
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, nil, nil)
m := newTestManager(tempDir, nil, nil, nil)
err := m.Remove("hello")
assert.NoError(t, err)
@ -195,7 +210,7 @@ func TestManager_Remove(t *testing.T) {
func TestManager_Upgrade_NoExtensions(t *testing.T) {
tempDir := t.TempDir()
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
m := newTestManager(tempDir, nil, nil, ios)
err := m.Upgrade("", false)
assert.EqualError(t, err, "no extensions installed")
assert.Equal(t, "", stdout.String())
@ -204,22 +219,42 @@ func TestManager_Upgrade_NoExtensions(t *testing.T) {
func TestManager_Upgrade_NoMatchingExtension(t *testing.T) {
tempDir := t.TempDir()
extDir := filepath.Join(tempDir, "extensions", "gh-hello")
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", extDir).Return(gcOne).Twice()
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
m := newTestManager(tempDir, nil, gc, ios)
err := m.Upgrade("invalid", false)
assert.EqualError(t, err, `no extension matched "invalid"`)
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_UpgradeExtensions(t *testing.T) {
tempDir := t.TempDir()
dirOne := filepath.Join(tempDir, "extensions", "gh-hello")
dirTwo := filepath.Join(tempDir, "extensions", "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(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", dirOne).Return(gcOne).Times(4)
gc.On("ForRepo", dirTwo).Return(gcTwo).Times(4)
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcTwo.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcTwo.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcOne.On("Remotes").Return(nil, nil).Once()
gcTwo.On("Remotes").Return(nil, nil).Once()
gcOne.On("Pull", "", "").Return(nil).Once()
gcTwo.On("Pull", "", "").Return(nil).Once()
m := newTestManager(tempDir, nil, gc, ios)
exts, err := m.list(false)
assert.NoError(t, err)
assert.Equal(t, 3, len(exts))
@ -229,27 +264,37 @@ func TestManager_UpgradeExtensions(t *testing.T) {
}
err = m.upgradeExtensions(exts, false)
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
assert.Equal(t, heredoc.Doc(
`
[hello]: [git -C %s pull --ff-only]
upgraded from old vers to new vers
[hello]: upgraded from old vers to new vers
[local]: local extensions can not be upgraded
[two]: [git -C %s pull --ff-only]
upgraded from old vers to new vers
[two]: upgraded from old vers to new vers
`,
filepath.Join(tempDir, "extensions", "gh-hello"),
filepath.Join(tempDir, "extensions", "gh-two"),
), stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
gcTwo.AssertExpectations(t)
}
func TestManager_UpgradeExtensions_DryRun(t *testing.T) {
tempDir := t.TempDir()
dirOne := filepath.Join(tempDir, "extensions", "gh-hello")
dirTwo := filepath.Join(tempDir, "extensions", "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(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", dirOne).Return(gcOne).Times(3)
gc.On("ForRepo", dirTwo).Return(gcTwo).Times(3)
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcTwo.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcTwo.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcOne.On("Remotes").Return(nil, nil).Once()
gcTwo.On("Remotes").Return(nil, nil).Once()
m := newTestManager(tempDir, nil, gc, ios)
m.EnableDryRunMode()
exts, err := m.list(false)
assert.NoError(t, err)
@ -268,6 +313,9 @@ func TestManager_UpgradeExtensions_DryRun(t *testing.T) {
`,
), stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
gcTwo.AssertExpectations(t)
}
func TestManager_UpgradeExtension_LocalExtension(t *testing.T) {
@ -275,7 +323,7 @@ func TestManager_UpgradeExtension_LocalExtension(t *testing.T) {
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
m := newTestManager(tempDir, nil, nil, ios)
exts, err := m.list(false)
assert.NoError(t, err)
assert.Equal(t, 1, len(exts))
@ -290,7 +338,7 @@ func TestManager_UpgradeExtension_LocalExtension_DryRun(t *testing.T) {
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
m := newTestManager(tempDir, nil, nil, ios)
m.EnableDryRunMode()
exts, err := m.list(false)
assert.NoError(t, err)
@ -303,9 +351,16 @@ func TestManager_UpgradeExtension_LocalExtension_DryRun(t *testing.T) {
func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
tempDir := t.TempDir()
extensionDir := filepath.Join(tempDir, "extensions", "gh-remote")
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", extensionDir).Return(gcOne).Times(4)
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcOne.On("Remotes").Return(nil, nil).Once()
gcOne.On("Pull", "", "").Return(nil).Once()
m := newTestManager(tempDir, nil, gc, ios)
exts, err := m.list(false)
assert.NoError(t, err)
assert.Equal(t, 1, len(exts))
@ -314,20 +369,23 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
ext.latestVersion = "new version"
err = m.upgradeExtension(ext, false)
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[git -C %s pull --ff-only]
`,
filepath.Join(tempDir, "extensions", "gh-remote"),
), stdout.String())
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_UpgradeExtension_GitExtension_DryRun(t *testing.T) {
tempDir := t.TempDir()
extDir := filepath.Join(tempDir, "extensions", "gh-remote")
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", extDir).Return(gcOne).Times(3)
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcOne.On("Remotes").Return(nil, nil).Once()
m := newTestManager(tempDir, nil, gc, ios)
m.EnableDryRunMode()
exts, err := m.list(false)
assert.NoError(t, err)
@ -339,6 +397,8 @@ func TestManager_UpgradeExtension_GitExtension_DryRun(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
@ -346,7 +406,14 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
extensionDir := filepath.Join(tempDir, "extensions", "gh-remote")
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", extensionDir).Return(gcOne).Times(4)
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
gcOne.On("Remotes").Return(nil, nil).Once()
gcOne.On("Fetch", "origin", "HEAD").Return(nil).Once()
gcOne.On("CommandOutput", []string{"reset", "--hard", "origin/HEAD"}).Return("", nil).Once()
m := newTestManager(tempDir, nil, gc, ios)
exts, err := m.list(false)
assert.NoError(t, err)
assert.Equal(t, 1, len(exts))
@ -355,14 +422,10 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
ext.latestVersion = "new version"
err = m.upgradeExtension(ext, true)
assert.NoError(t, err)
assert.Equal(t, heredoc.Docf(
`
[git -C %[1]s fetch origin HEAD]
[git -C %[1]s reset --hard origin/HEAD]
`,
extensionDir,
), stdout.String())
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_MigrateToBinaryExtension(t *testing.T) {
@ -373,7 +436,8 @@ func TestManager_MigrateToBinaryExtension(t *testing.T) {
reg := httpmock.Registry{}
defer reg.Verify(t)
client := http.Client{Transport: &reg}
m := newTestManager(tempDir, &client, ios)
gc := &gitExecuter{client: &git.Client{}}
m := newTestManager(tempDir, &client, gc, ios)
exts, err := m.list(false)
assert.NoError(t, err)
assert.Equal(t, 1, len(exts))
@ -458,7 +522,7 @@ func TestManager_UpgradeExtension_BinaryExtension(t *testing.T) {
}))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, ios)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, nil, ios)
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
@ -523,7 +587,7 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned_Force(t *testing.T) {
}))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, ios)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, nil, ios)
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
httpmock.JSONResponse(
@ -585,7 +649,7 @@ func TestManager_UpgradeExtension_BinaryExtension_DryRun(t *testing.T) {
}))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, ios)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, nil, ios)
m.EnableDryRunMode()
reg.Register(
httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"),
@ -638,7 +702,7 @@ func TestManager_UpgradeExtension_BinaryExtension_Pinned(t *testing.T) {
}))
ios, _, _, _ := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
m := newTestManager(tempDir, nil, nil, ios)
exts, err := m.list(false)
assert.Nil(t, err)
assert.Equal(t, 1, len(exts))
@ -655,8 +719,16 @@ func TestManager_UpgradeExtenion_GitExtension_Pinned(t *testing.T) {
assert.NoError(t, stubPinnedExtension(filepath.Join(extDir, "gh-remote"), "abcd1234"))
ios, _, _, _ := iostreams.Test()
m := newTestManager(tempDir, nil, ios)
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", extDir).Return(gcOne).Twice()
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
m := newTestManager(tempDir, nil, gc, ios)
exts, err := m.list(false)
assert.NoError(t, err)
assert.Equal(t, 1, len(exts))
ext := exts[0]
@ -666,6 +738,8 @@ func TestManager_UpgradeExtenion_GitExtension_Pinned(t *testing.T) {
err = m.upgradeExtension(ext, false)
assert.NotNil(t, err)
assert.Equal(t, err, pinnedExtensionUpgradeError)
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_Install_git(t *testing.T) {
@ -676,7 +750,12 @@ func TestManager_Install_git(t *testing.T) {
client := http.Client{Transport: &reg}
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(tempDir, &client, ios)
extensionDir := filepath.Join(tempDir, "extensions", "gh-some-ext")
gc := &mockGitClient{}
gc.On("Clone", "https://github.com/owner/gh-some-ext.git", []string{extensionDir}).Return("", nil).Once()
m := newTestManager(tempDir, &client, gc, ios)
reg.Register(
httpmock.REST("GET", "repos/owner/gh-some-ext/releases/latest"),
@ -697,8 +776,9 @@ func TestManager_Install_git(t *testing.T) {
err := m.Install(repo, "")
assert.NoError(t, err)
assert.Equal(t, fmt.Sprintf("[git clone https://github.com/owner/gh-some-ext.git %s]\n", filepath.Join(tempDir, "extensions", "gh-some-ext")), stdout.String())
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
}
func TestManager_Install_git_pinned(t *testing.T) {
@ -708,8 +788,15 @@ func TestManager_Install_git_pinned(t *testing.T) {
defer reg.Verify(t)
client := http.Client{Transport: &reg}
ios, _, _, stderr := iostreams.Test()
m := newTestManager(tempDir, &client, ios)
ios, _, stdout, stderr := iostreams.Test()
extensionDir := filepath.Join(tempDir, "extensions", "gh-cool-ext")
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", extensionDir).Return(gcOne).Once()
gc.On("Clone", "https://github.com/owner/gh-cool-ext.git", []string{extensionDir}).Return("", nil).Once()
gcOne.On("CheckoutBranch", "abcd1234").Return(nil).Once()
m := newTestManager(tempDir, &client, gc, ios)
reg.Register(
httpmock.REST("GET", "repos/owner/gh-cool-ext/releases/latest"),
@ -734,6 +821,9 @@ func TestManager_Install_git_pinned(t *testing.T) {
err := m.Install(repo, "some-ref")
assert.NoError(t, err)
assert.Equal(t, "", stderr.String())
assert.Equal(t, "", stdout.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_Install_binary_pinned(t *testing.T) {
@ -772,7 +862,7 @@ func TestManager_Install_binary_pinned(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, ios)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, nil, ios)
err := m.Install(repo, "v1.6.3-pre")
assert.NoError(t, err)
@ -836,7 +926,7 @@ func TestManager_Install_binary_unsupported(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &client, ios)
m := newTestManager(tempDir, &client, nil, ios)
err := m.Install(repo, "")
assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`")
@ -881,7 +971,7 @@ func TestManager_Install_binary(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, ios)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, nil, ios)
err := m.Install(repo, "")
assert.NoError(t, err)
@ -925,7 +1015,7 @@ func TestManager_repo_not_found(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
tempDir := t.TempDir()
m := newTestManager(tempDir, &http.Client{Transport: &reg}, ios)
m := newTestManager(tempDir, &http.Client{Transport: &reg}, nil, ios)
if err := m.Install(repo, ""); err != repositoryNotFoundErr {
t.Errorf("expected repositoryNotFoundErr, got: %v", err)
@ -937,24 +1027,35 @@ func TestManager_repo_not_found(t *testing.T) {
func TestManager_Create(t *testing.T) {
chdirTemp(t)
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", nil, ios)
err := os.MkdirAll("gh-test", 0755)
assert.NoError(t, err)
err := m.Create("gh-test", extensions.GitTemplateType)
ios, _, stdout, stderr := iostreams.Test()
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", "gh-test").Return(gcOne).Once()
gc.On("CommandOutput", []string{"init", "--quiet", "gh-test"}).Return("", nil).Once()
gcOne.On("CommandOutput", []string{"add", "gh-test", "--chmod=+x"}).Return("", nil).Once()
m := newTestManager(".", nil, gc, ios)
err = m.Create("gh-test", extensions.GitTemplateType)
assert.NoError(t, err)
files, err := os.ReadDir("gh-test")
assert.NoError(t, err)
assert.Equal(t, []string{"gh-test"}, fileNames(files))
assert.Equal(t, heredoc.Doc(`
[git init --quiet gh-test]
[git -C gh-test add gh-test --chmod=+x]
`), stdout.String())
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_Create_go_binary(t *testing.T) {
chdirTemp(t)
err := os.MkdirAll("gh-test", 0755)
assert.NoError(t, err)
reg := httpmock.Registry{}
defer reg.Verify(t)
reg.Register(
@ -962,9 +1063,15 @@ func TestManager_Create_go_binary(t *testing.T) {
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", &http.Client{Transport: &reg}, ios)
err := m.Create("gh-test", extensions.GoBinTemplateType)
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", "gh-test").Return(gcOne).Once()
gc.On("CommandOutput", []string{"init", "--quiet", "gh-test"}).Return("", nil).Once()
gcOne.On("CommandOutput", []string{"add", "."}).Return("", nil).Once()
m := newTestManager(".", &http.Client{Transport: &reg}, gc, ios)
err = m.Create("gh-test", extensions.GoBinTemplateType)
require.NoError(t, err)
files, err := os.ReadDir("gh-test")
@ -983,21 +1090,31 @@ func TestManager_Create_go_binary(t *testing.T) {
assert.Equal(t, []string{"release.yml"}, fileNames(files))
assert.Equal(t, heredoc.Doc(`
[git init --quiet gh-test]
[go mod init github.com/jillv/gh-test]
[go mod tidy]
[go build]
[git -C gh-test add .]
`), stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
func TestManager_Create_other_binary(t *testing.T) {
chdirTemp(t)
ios, _, stdout, stderr := iostreams.Test()
m := newTestManager(".", nil, ios)
err := os.MkdirAll("gh-test", 0755)
assert.NoError(t, err)
err := m.Create("gh-test", extensions.OtherBinTemplateType)
ios, _, stdout, stderr := iostreams.Test()
gc, gcOne := &mockGitClient{}, &mockGitClient{}
gc.On("ForRepo", "gh-test").Return(gcOne).Once()
gc.On("CommandOutput", []string{"init", "--quiet", "gh-test"}).Return("", nil).Once()
gcOne.On("CommandOutput", []string{"add", filepath.Join("script", "build.sh"), "--chmod=+x"}).Return("", nil).Once()
gcOne.On("CommandOutput", []string{"add", "."}).Return("", nil).Once()
m := newTestManager(".", nil, gc, ios)
err = m.Create("gh-test", extensions.OtherBinTemplateType)
assert.NoError(t, err)
files, err := os.ReadDir("gh-test")
@ -1012,12 +1129,10 @@ func TestManager_Create_other_binary(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, []string{"build.sh"}, fileNames(files))
assert.Equal(t, heredoc.Docf(`
[git init --quiet gh-test]
[git -C gh-test add %s --chmod=+x]
[git -C gh-test add .]
`, filepath.FromSlash("script/build.sh")), stdout.String())
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
gc.AssertExpectations(t)
gcOne.AssertExpectations(t)
}
// chdirTemp changes the current working directory to a temporary directory for the duration of the test.

View file

@ -0,0 +1,53 @@
package extension
import (
"github.com/cli/cli/v2/git"
"github.com/stretchr/testify/mock"
)
type mockGitClient struct {
mock.Mock
}
func (g *mockGitClient) CheckoutBranch(branch string) error {
args := g.Called(branch)
return args.Error(0)
}
func (g *mockGitClient) Clone(cloneURL string, cloneArgs []string) (string, error) {
args := g.Called(cloneURL, cloneArgs)
return args.String(0), args.Error(1)
}
func (g *mockGitClient) CommandOutput(commandArgs []string) ([]byte, error) {
args := g.Called(commandArgs)
return []byte(args.String(0)), args.Error(1)
}
func (g *mockGitClient) Config(name string) (string, error) {
args := g.Called(name)
return args.String(0), args.Error(1)
}
func (g *mockGitClient) Fetch(remote string, refspec string) error {
args := g.Called(remote, refspec)
return args.Error(0)
}
func (g *mockGitClient) ForRepo(repoDir string) gitClient {
args := g.Called(repoDir)
if v, ok := args.Get(0).(*mockGitClient); ok {
return v
}
return nil
}
func (g *mockGitClient) Pull(remote, branch string) error {
args := g.Called(remote, branch)
return args.Error(0)
}
func (g *mockGitClient) Remotes() (git.RemoteSet, error) {
args := g.Called()
return nil, args.Error(1)
}

View file

@ -156,7 +156,7 @@ func branchFunc(f *cmdutil.Factory) func() (string, error) {
}
func extensionManager(f *cmdutil.Factory) *extension.Manager {
em := extension.NewManager(f.IOStreams)
em := extension.NewManager(f.IOStreams, f.GitClient)
cfg, err := f.Config()
if err != nil {