Repository: cli/cli
Pull request #7567 (2023-06-11 23:07:26)

Fix error handling for extension and shell alias commands
Catching `*exec.ExitError` in order to return the correct exit code was too broad and stopping execution for commands like `release create` which execute `git` commands, this PR limits this behavior to only extension and shell commands. This is the same behavior that existed prior to the nested aliases change.

Fixes https://github.com/cli/cli/issues/7556


Patch:
diff --git a/cmd/gh/main.go b/cmd/gh/main.go
index c36a4e6e6..15af90721 100644
--- a/cmd/gh/main.go
+++ b/cmd/gh/main.go
@@ -109,7 +109,7 @@ func mainRun() exitCode {
 	if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
 		var pagerPipeError *iostreams.ErrClosedPagerPipe
 		var noResultsError cmdutil.NoResultsError
-		var execError *exec.ExitError
+		var extError *root.ExternalCommandExitError
 		var authError *root.AuthError
 		if err == cmdutil.SilentError {
 			return exitError
@@ -130,8 +130,9 @@ func mainRun() exitCode {
 			}
 			// no results is not a command failure
 			return exitOK
-		} else if errors.As(err, &execError) {
-			return exitCode(execError.ExitCode())
+		} else if errors.As(err, &extError) {
+			// pass on exit codes from extensions and shell aliases
+			return exitCode(extError.ExitCode())
 		}
 
 		printError(stderr, err, cmd, hasDebug)
diff --git a/git/client.go b/git/client.go
index a7d59899e..a029c8ba6 100644
--- a/git/client.go
+++ b/git/client.go
@@ -36,6 +36,19 @@ type Client struct {
 	mu             sync.Mutex
 }
 
+func (c *Client) Copy() *Client {
+	return &Client{
+		GhPath:  c.GhPath,
+		RepoDir: c.RepoDir,
+		GitPath: c.GitPath,
+		Stderr:  c.Stderr,
+		Stdin:   c.Stdin,
+		Stdout:  c.Stdout,
+
+		commandContext: c.commandContext,
+	}
+}
+
 func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) {
 	if c.RepoDir != "" {
 		args = append([]string{"-C", c.RepoDir}, args...)
@@ -408,6 +421,44 @@ func (c *Client) revParse(ctx context.Context, args ...string) ([]byte, error) {
 	return cmd.Output()
 }
 
+func (c *Client) IsLocalGitRepo(ctx context.Context) (bool, error) {
+	_, err := c.GitDir(ctx)
+	if err != nil {
+		var execError errWithExitCode
+		if errors.As(err, &execError) && execError.ExitCode() == 128 {
+			return false, nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error {
+	args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)}
+	cmd, err := c.Command(ctx, args...)
+	if err != nil {
+		return err
+	}
+	_, err = cmd.Output()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Client) SetRemoteBranches(ctx context.Context, remote string, refspec string) error {
+	args := []string{"remote", "set-branches", remote, refspec}
+	cmd, err := c.Command(ctx, args...)
+	if err != nil {
+		return err
+	}
+	_, err = cmd.Output()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // Below are commands that make network calls and need authentication credentials supplied from gh.
 
 func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods ...CommandModifier) error {
@@ -513,31 +564,6 @@ func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBra
 	return remote, nil
 }
 
-func (c *Client) IsLocalGitRepo(ctx context.Context) (bool, error) {
-	_, err := c.GitDir(ctx)
-	if err != nil {
-		var execError errWithExitCode
-		if errors.As(err, &execError) && execError.ExitCode() == 128 {
-			return false, nil
-		}
-		return false, err
-	}
-	return true, nil
-}
-
-func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error {
-	args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)}
-	cmd, err := c.Command(ctx, args...)
-	if err != nil {
-		return err
-	}
-	_, err = cmd.Output()
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
 func resolveGitPath() (string, error) {
 	path, err := safeexec.LookPath("git")
 	if err != nil {
diff --git a/go.mod b/go.mod
index a0b3781cf..e0e92c352 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/cli/cli/v2
 go 1.19
 
 require (
-	github.com/AlecAivazis/survey/v2 v2.3.6
+	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/MakeNowJust/heredoc v1.0.0
 	github.com/briandowns/spinner v1.18.1
 	github.com/cenkalti/backoff/v4 v4.2.1
diff --git a/go.sum b/go.sum
index 1d3b83805..85ba744f4 100644
--- a/go.sum
+++ b/go.sum
@@ -31,8 +31,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
-github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
+github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
+github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -401,7 +401,6 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -410,7 +409,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
@@ -421,6 +419,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go
index 9d1d61c21..188687634 100644
--- a/pkg/cmd/extension/extension.go
+++ b/pkg/cmd/extension/extension.go
@@ -1,8 +1,16 @@
 package extension
 
 import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"os"
 	"path/filepath"
 	"strings"
+	"sync"
+
+	"github.com/cli/cli/v2/internal/ghrepo"
+	"gopkg.in/yaml.v3"
 )
 
 const manifestName = "manifest.yml"
@@ -12,16 +20,22 @@ type ExtensionKind int
 const (
 	GitKind ExtensionKind = iota
 	BinaryKind
+	LocalKind
 )
 
 type Extension struct {
-	path           string
+	path       string
+	kind       ExtensionKind
+	gitClient  gitClient
+	httpClient *http.Client
+
+	mu sync.RWMutex
+
+	// These fields get resolved dynamically:
 	url            string
-	isLocal        bool
-	isPinned       bool
+	isPinned       *bool
 	currentVersion string
 	latestVersion  string
-	kind           ExtensionKind
 }
 
 func (e *Extension) Name() string {
@@ -32,32 +46,157 @@ func (e *Extension) Path() string {
 	return e.path
 }
 
-func (e *Extension) URL() string {
-	return e.url
+func (e *Extension) IsLocal() bool {
+	return e.kind == LocalKind
 }
 
-func (e *Extension) IsLocal() bool {
-	return e.isLocal
+func (e *Extension) IsBinary() bool {
+	return e.kind == BinaryKind
+}
+
+func (e *Extension) URL() string {
+	e.mu.RLock()
+	if e.url != "" {
+		defer e.mu.RUnlock()
+		return e.url
+	}
+	e.mu.RUnlock()
+
+	var url string
+	switch e.kind {
+	case LocalKind:
+	case BinaryKind:
+		if manifest, err := e.loadManifest(); err == nil {
+			repo := ghrepo.NewWithHost(manifest.Owner, manifest.Name, manifest.Host)
+			url = ghrepo.GenerateRepoURL(repo, "")
+		}
+	case GitKind:
+		if remoteURL, err := e.gitClient.Config("remote.origin.url"); err == nil {
+			url = strings.TrimSpace(string(remoteURL))
+		}
+	}
+
+	e.mu.Lock()
+	e.url = url
+	e.mu.Unlock()
+
+	return e.url
 }
 
 func (e *Extension) CurrentVersion() string {
+	e.mu.RLock()
+	if e.currentVersion != "" {
+		defer e.mu.RUnlock()
+		return e.currentVersion
+	}
+	e.mu.RUnlock()
+
+	var currentVersion string
+	switch e.kind {
+	case LocalKind:
+	case BinaryKind:
+		if manifest, err := e.loadManifest(); err == nil {
+			currentVersion = manifest.Tag
+		}
+	case GitKind:
+		if sha, err := e.gitClient.CommandOutput([]string{"rev-parse", "HEAD"}); err == nil {
+			currentVersion = string(bytes.TrimSpace(sha))
+		}
+	}
+
+	e.mu.Lock()
+	e.currentVersion = currentVersion
+	e.mu.Unlock()
+
 	return e.currentVersion
 }
 
+func (e *Extension) LatestVersion() string {
+	e.mu.RLock()
+	if e.latestVersion != "" {
+		defer e.mu.RUnlock()
+		return e.latestVersion
+	}
+	e.mu.RUnlock()
+
+	var latestVersion string
+	switch e.kind {
+	case LocalKind:
+	case BinaryKind:
+		repo, err := ghrepo.FromFullName(e.URL())
+		if err != nil {
+			return ""
+		}
+		release, err := fetchLatestRelease(e.httpClient, repo)
+		if err != nil {
+			return ""
+		}
+		latestVersion = release.Tag
+	case GitKind:
+		if lsRemote, err := e.gitClient.CommandOutput([]string{"ls-remote", "origin", "HEAD"}); err == nil {
+			latestVersion = string(bytes.SplitN(lsRemote, []byte("\t"), 2)[0])
+		}
+	}
+
+	e.mu.Lock()
+	e.latestVersion = latestVersion
+	e.mu.Unlock()
+
+	return e.latestVersion
+}
+
 func (e *Extension) IsPinned() bool {
-	return e.isPinned
+	e.mu.RLock()
+	if e.isPinned != nil {
+		defer e.mu.RUnlock()
+		return *e.isPinned
+	}
+	e.mu.RUnlock()
+
+	var isPinned bool
+	switch e.kind {
+	case LocalKind:
+	case BinaryKind:
+		if manifest, err := e.loadManifest(); err == nil {
+			isPinned = manifest.IsPinned
+		}
+	case GitKind:
+		pinPath := filepath.Join(e.Path(), fmt.Sprintf(".pin-%s", e.CurrentVersion()))
+		if _, err := os.Stat(pinPath); err == nil {
+			isPinned = true
+		} else {
+			isPinned = false
+		}
+	}
+
+	e.mu.Lock()
+	e.isPinned = &isPinned
+	e.mu.Unlock()
+
+	return *e.isPinned
 }
 
 func (e *Extension) UpdateAvailable() bool {
-	if e.isLocal ||
-		e.currentVersion == "" ||
-		e.latestVersion == "" ||
-		e.currentVersion == e.latestVersion {
+	if e.IsLocal() ||
+		e.CurrentVersion() == "" ||
+		e.LatestVersion() == "" ||
+		e.CurrentVersion() == e.LatestVersion() {
 		return false
 	}
 	return true
 }
 
-func (e *Extension) IsBinary() bool {
-	return e.kind == BinaryKind
+func (e *Extension) loadManifest() (binManifest, error) {
+	var bm binManifest
+	dir, _ := filepath.Split(e.Path())
+	manifestPath := filepath.Join(dir, manifestName)
+	manifest, err := os.ReadFile(manifestPath)
+	if err != nil {
+		return bm, fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
+	}
+	err = yaml.Unmarshal(manifest, &bm)
+	if err != nil {
+		return bm, fmt.Errorf("could not parse %s: %w", manifestPath, err)
+	}
+	return bm, nil
 }
diff --git a/pkg/cmd/extension/git.go b/pkg/cmd/extension/git.go
index e85ff7371..58ef0ca12 100644
--- a/pkg/cmd/extension/git.go
+++ b/pkg/cmd/extension/git.go
@@ -46,16 +46,9 @@ func (g *gitExecuter) Fetch(remote string, refspec string) error {
 }
 
 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,
-		},
-	}
+	gc := g.client.Copy()
+	gc.RepoDir = repoDir
+	return &gitExecuter{client: gc}
 }
 
 func (g *gitExecuter) Pull(remote, branch string) error {
diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go
index 7a76e6a47..b19175880 100644
--- a/pkg/cmd/extension/manager.go
+++ b/pkg/cmd/extension/manager.go
@@ -1,12 +1,10 @@
 package extension
 
 import (
-	"bytes"
 	_ "embed"
 	"errors"
 	"fmt"
 	"io"
-	"io/fs"
 	"net/http"
 	"os"
 	"os/exec"
@@ -83,7 +81,7 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
 	forwardArgs := args[1:]
 
 	exts, _ := m.list(false)
-	var ext Extension
+	var ext *Extension
 	for _, e := range exts {
 		if e.Name() == extName {
 			ext = e
@@ -121,39 +119,53 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
 func (m *Manager) List() []extensions.Extension {
 	exts, _ := m.list(false)
 	r := make([]extensions.Extension, len(exts))
-	for i, v := range exts {
-		val := v
-		r[i] = &val
+	for i, ext := range exts {
+		r[i] = ext
 	}
 	return r
 }
 
-func (m *Manager) list(includeMetadata bool) ([]Extension, error) {
+func (m *Manager) list(includeMetadata bool) ([]*Extension, error) {
 	dir := m.installDir()
 	entries, err := os.ReadDir(dir)
 	if err != nil {
 		return nil, err
 	}
 
-	var results []Extension
+	results := make([]*Extension, 0, len(entries))
 	for _, f := range entries {
 		if !strings.HasPrefix(f.Name(), "gh-") {
 			continue
 		}
-		var ext Extension
-		var err error
 		if f.IsDir() {
-			ext, err = m.parseExtensionDir(f)
-			if err != nil {
-				return nil, err
+			if _, err := os.Stat(filepath.Join(dir, f.Name(), manifestName)); err == nil {
+				results = append(results, &Extension{
+					path:       filepath.Join(dir, f.Name(), f.Name()),
+					kind:       BinaryKind,
+					httpClient: m.client,
+				})
+			} else {
+				results = append(results, &Extension{
+					path:      filepath.Join(dir, f.Name(), f.Name()),
+					kind:      GitKind,
+					gitClient: m.gitClient.ForRepo(filepath.Join(dir, f.Name())),
+				})
 			}
-			results = append(results, ext)
+		} else if isSymlink(f.Type()) {
+			results = append(results, &Extension{
+				path: filepath.Join(dir, f.Name(), f.Name()),
+				kind: LocalKind,
+			})
 		} else {
-			ext, err = m.parseExtensionFile(f)
+			// the contents of a regular file point to a local extension on disk
+			p, err := readPathFromFile(filepath.Join(dir, f.Name()))
 			if err != nil {
 				return nil, err
 			}
-			results = append(results, ext)
+			results = append(results, &Extension{
+				path: filepath.Join(p, f.Name()),
+				kind: LocalKind,
+			})
 		}
 	}
 
@@ -164,145 +176,16 @@ func (m *Manager) list(includeMetadata bool) ([]Extension, error) {
 	return results, nil
 }
 
-func (m *Manager) parseExtensionFile(fi fs.DirEntry) (Extension, error) {
-	ext := Extension{isLocal: true}
-	id := m.installDir()
-	exePath := filepath.Join(id, fi.Name(), fi.Name())
-	if !isSymlink(fi.Type()) {
-		// if this is a regular file, its contents is the local directory of the extension
-		p, err := readPathFromFile(filepath.Join(id, fi.Name()))
-		if err != nil {
-			return ext, err
-		}
-		exePath = filepath.Join(p, fi.Name())
-	}
-	ext.path = exePath
-	return ext, nil
-}
-
-func (m *Manager) parseExtensionDir(fi fs.DirEntry) (Extension, error) {
-	id := m.installDir()
-	if _, err := os.Stat(filepath.Join(id, fi.Name(), manifestName)); err == nil {
-		return m.parseBinaryExtensionDir(fi)
-	}
-
-	return m.parseGitExtensionDir(fi)
-}
-
-func (m *Manager) parseBinaryExtensionDir(fi fs.DirEntry) (Extension, error) {
-	id := m.installDir()
-	exePath := filepath.Join(id, fi.Name(), fi.Name())
-	ext := Extension{path: exePath, kind: BinaryKind}
-	manifestPath := filepath.Join(id, fi.Name(), manifestName)
-	manifest, err := os.ReadFile(manifestPath)
-	if err != nil {
-		return ext, fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
-	}
-	var bm binManifest
-	err = yaml.Unmarshal(manifest, &bm)
-	if err != nil {
-		return ext, fmt.Errorf("could not parse %s: %w", manifestPath, err)
-	}
-	repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host)
-	remoteURL := ghrepo.GenerateRepoURL(repo, "")
-	ext.url = remoteURL
-	ext.currentVersion = bm.Tag
-	ext.isPinned = bm.IsPinned
-	return ext, nil
-}
-
-func (m *Manager) parseGitExtensionDir(fi fs.DirEntry) (Extension, error) {
-	id := m.installDir()
-	exePath := filepath.Join(id, fi.Name(), fi.Name())
-	remoteUrl := m.getRemoteUrl(fi.Name())
-	currentVersion := m.getCurrentVersion(fi.Name())
-
-	var isPinned bool
-	pinPath := filepath.Join(id, fi.Name(), fmt.Sprintf(".pin-%s", currentVersion))
-	if _, err := os.Stat(pinPath); err == nil {
-		isPinned = true
-	}
-
-	return Extension{
-		path:           exePath,
-		url:            remoteUrl,
-		isLocal:        false,
-		currentVersion: currentVersion,
-		kind:           GitKind,
-		isPinned:       isPinned,
-	}, nil
-}
-
-// getCurrentVersion determines the current version for non-local git extensions.
-func (m *Manager) getCurrentVersion(extension string) string {
-	dir := filepath.Join(m.installDir(), extension)
-	scopedClient := m.gitClient.ForRepo(dir)
-	localSha, err := scopedClient.CommandOutput([]string{"rev-parse", "HEAD"})
-	if err != nil {
-		return ""
-	}
-	return string(bytes.TrimSpace(localSha))
-}
-
-// getRemoteUrl determines the remote URL for non-local git extensions.
-func (m *Manager) getRemoteUrl(extension string) string {
-	dir := filepath.Join(m.installDir(), extension)
-	scopedClient := m.gitClient.ForRepo(dir)
-	url, err := scopedClient.Config("remote.origin.url")
-	if err != nil {
-		return ""
-	}
-	return strings.TrimSpace(string(url))
-}
-
-func (m *Manager) populateLatestVersions(exts []Extension) {
-	size := len(exts)
-	type result struct {
-		index   int
-		version string
-	}
-	ch := make(chan result, size)
+func (m *Manager) populateLatestVersions(exts []*Extension) {
 	var wg sync.WaitGroup
-	wg.Add(size)
-	for idx, ext := range exts {
-		go func(i int, e Extension) {
+	for _, ext := range exts {
+		wg.Add(1)
+		go func(e *Extension) {
 			defer wg.Done()
-			version, _ := m.getLatestVersion(e)
-			ch <- result{index: i, version: version}
-		}(idx, ext)
+			e.LatestVersion()
+		}(ext)
 	}
 	wg.Wait()
-	close(ch)
-	for r := range ch {
-		ext := &exts[r.index]
-		ext.latestVersion = r.version
-	}
-}
-
-func (m *Manager) getLatestVersion(ext Extension) (string, error) {
-	if ext.isLocal {
-		return "", localExtensionUpgradeError
-	}
-	if ext.IsBinary() {
-		repo, err := ghrepo.FromFullName(ext.url)
-		if err != nil {
-			return "", err
-		}
-		r, err := fetchLatestRelease(m.client, repo)
-		if err != nil {
-			return "", err
-		}
-		return r.Tag, nil
-	} else {
-		extDir := filepath.Dir(ext.path)
-		scopedClient := m.gitClient.ForRepo(extDir)
-		lsRemote, err := scopedClient.CommandOutput([]string{"ls-remote", "origin", "HEAD"})
-		if err != nil {
-			return "", err
-		}
-		remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0]
-		return string(remoteSha), nil
-	}
 }
 
 func (m *Manager) InstallLocal(dir string) error {
@@ -521,22 +404,23 @@ func (m *Manager) Upgrade(name string, force bool) error {
 		if f.Name() != name {
 			continue
 		}
-		var err error
-		// For single extensions manually retrieve latest version since we forgo
-		// doing it during list.
-		f.latestVersion, err = m.getLatestVersion(f)
-		if err != nil {
-			return err
+		if f.IsLocal() {
+			return localExtensionUpgradeError
+		}
+		// For single extensions manually retrieve latest version since we forgo doing it during list.
+		if latestVersion := f.LatestVersion(); latestVersion == "" {
+			return fmt.Errorf("unable to retrieve latest version for extension %q", name)
 		}
-		return m.upgradeExtensions([]Extension{f}, force)
+		return m.upgradeExtensions([]*Extension{f}, force)
 	}
 	return fmt.Errorf("no extension matched %q", name)
 }
 
-func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
+func (m *Manager) upgradeExtensions(exts []*Extension, force bool) error {
 	var failed bool
 	for _, f := range exts {
 		fmt.Fprintf(m.io.Out, "[%s]: ", f.Name())
+		currentVersion := displayExtensionVersion(f, f.CurrentVersion())
 		err := m.upgradeExtension(f, force)
 		if err != nil {
 			if !errors.Is(err, localExtensionUpgradeError) &&
@@ -547,8 +431,7 @@ func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
 			fmt.Fprintf(m.io.Out, "%s\n", err)
 			continue
 		}
-		currentVersion := displayExtensionVersion(&f, f.currentVersion)
-		latestVersion := displayExtensionVersion(&f, f.latestVersion)
+		latestVersion := displayExtensionVersion(f, f.LatestVersion())
 		if m.dryRunMode {
 			fmt.Fprintf(m.io.Out, "would have upgraded from %s to %s\n", currentVersion, latestVersion)
 		} else {
@@ -561,8 +444,8 @@ func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
 	return nil
 }
 
-func (m *Manager) upgradeExtension(ext Extension, force bool) error {
-	if ext.isLocal {
+func (m *Manager) upgradeExtension(ext *Extension, force bool) error {
+	if ext.IsLocal() {
 		return localExtensionUpgradeError
 	}
 	if !force && ext.IsPinned() {
@@ -592,7 +475,7 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
 	return err
 }
 
-func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
+func (m *Manager) upgradeGitExtension(ext *Extension, force bool) error {
 	if m.dryRunMode {
 		return nil
 	}
@@ -611,10 +494,10 @@ func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
 	return scopedClient.Pull("", "")
 }
 
-func (m *Manager) upgradeBinExtension(ext Extension) error {
-	repo, err := ghrepo.FromFullName(ext.url)
+func (m *Manager) upgradeBinExtension(ext *Extension) error {
+	repo, err := ghrepo.FromFullName(ext.URL())
 	if err != nil {
-		return fmt.Errorf("failed to parse URL %s: %w", ext.url, err)
+		return fmt.Errorf("failed to parse URL %s: %w", ext.URL(), err)
 	}
 	return m.installBin(repo, "")
 }
diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go
index 2414c1807..a4db9d3a1 100644
--- a/pkg/cmd/issue/create/create.go
+++ b/pkg/cmd/issue/create/create.go
@@ -11,7 +11,6 @@ import (
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/text"
-	"github.com/cli/cli/v2/pkg/cmd/pr/shared"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -143,7 +142,7 @@ func createRun(opts *CreateOptions) (err error) {
 		milestones = []string{opts.Milestone}
 	}
 
-	meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost())
+	meReplacer := prShared.NewMeReplacer(apiClient, baseRepo.RepoHost())
 	assignees, err := meReplacer.ReplaceSlice(opts.Assignees)
 	if err != nil {
 		return err
@@ -167,7 +166,7 @@ func createRun(opts *CreateOptions) (err error) {
 		}
 	}
 
-	tpl := shared.NewTemplateManager(httpClient, baseRepo, opts.Prompter, opts.RootDirOverride, !opts.HasRepoOverride, false)
+	tpl := prShared.NewTemplateManager(httpClient, baseRepo, opts.Prompter, opts.RootDirOverride, !opts.HasRepoOverride, false)
 
 	if opts.WebMode {
 		var openURL string
@@ -222,7 +221,7 @@ func createRun(opts *CreateOptions) (err error) {
 			templateContent := ""
 
 			if opts.RecoverFile == "" {
-				var template shared.Template
+				var template prShared.Template
 
 				if opts.Template != "" {
 					template, err = tpl.Select(opts.Template)
@@ -325,7 +324,7 @@ func createRun(opts *CreateOptions) (err error) {
 	return
 }
 
-func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb shared.IssueMetadataState) (string, error) {
+func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState) (string, error) {
 	openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
 	return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb)
 }
diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go
index 71ed5ed0f..20e77e141 100644
--- a/pkg/cmd/issue/list/list.go
+++ b/pkg/cmd/issue/list/list.go
@@ -15,7 +15,6 @@ import (
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/text"
 	issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared"
-	"github.com/cli/cli/v2/pkg/cmd/pr/shared"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -133,7 +132,7 @@ func listRun(opts *ListOptions) error {
 	}
 
 	issueState := strings.ToLower(opts.State)
-	if issueState == "open" && shared.QueryHasStateClause(opts.Search) {
+	if issueState == "open" && prShared.QueryHasStateClause(opts.Search) {
 		issueState = ""
 	}
 
@@ -228,7 +227,7 @@ func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.Filt
 	}
 
 	var err error
-	meReplacer := shared.NewMeReplacer(apiClient, repo.RepoHost())
+	meReplacer := prShared.NewMeReplacer(apiClient, repo.RepoHost())
 	filters.Assignee, err = meReplacer.Replace(filters.Assignee)
 	if err != nil {
 		return nil, err
diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go
index fc39013c0..8d04ac630 100644
--- a/pkg/cmd/repo/clone/clone.go
+++ b/pkg/cmd/repo/clone/clone.go
@@ -175,12 +175,19 @@ func cloneRun(opts *CloneOptions) error {
 			upstreamName = canonicalRepo.Parent.RepoOwner()
 		}
 
-		_, err = gitClient.AddRemote(ctx, upstreamName, upstreamURL, []string{canonicalRepo.Parent.DefaultBranchRef.Name}, git.WithRepoDir(cloneDir))
+		gc := gitClient.Copy()
+		gc.RepoDir = cloneDir
+
+		_, err = gc.AddRemote(ctx, upstreamName, upstreamURL, []string{canonicalRepo.Parent.DefaultBranchRef.Name})
 		if err != nil {
 			return err
 		}
 
-		if err := gitClient.Fetch(ctx, upstreamName, "", git.WithRepoDir(cloneDir)); err != nil {
+		if err := gc.Fetch(ctx, upstreamName, ""); err != nil {
+			return err
+		}
+
+		if err := gc.SetRemoteBranches(ctx, upstreamName, `*`); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go
index fe2269e7e..1bd37769b 100644
--- a/pkg/cmd/repo/create/create.go
+++ b/pkg/cmd/repo/create/create.go
@@ -662,8 +662,8 @@ func localInit(gitClient *git.Client, remoteURL, path string) error {
 		return err
 	}
 
-	// Clone the client so we do not modify the original client's RepoDir.
-	gc := cloneGitClient(gitClient)
+	// Copy the client so we do not modify the original client's RepoDir.
+	gc := gitClient.Copy()
 	gc.RepoDir = path
 
 	gitRemoteAdd, err := gc.Command(ctx, "remote", "add", "origin", remoteURL)
@@ -794,14 +794,3 @@ func splitNameAndOwner(name string) (string, string, error) {
 	}
 	return repo.RepoName(), repo.RepoOwner(), nil
 }
-
-func cloneGitClient(c *git.Client) *git.Client {
-	return &git.Client{
-		GhPath:  c.GhPath,
-		RepoDir: c.RepoDir,
-		GitPath: c.GitPath,
-		Stderr:  c.Stderr,
-		Stdin:   c.Stdin,
-		Stdout:  c.Stdout,
-	}
-}
diff --git a/pkg/cmd/root/alias.go b/pkg/cmd/root/alias.go
index ba0bfe9e2..4f504f2b8 100644
--- a/pkg/cmd/root/alias.go
+++ b/pkg/cmd/root/alias.go
@@ -31,6 +31,10 @@ func NewCmdShellAlias(io *iostreams.IOStreams, aliasName, aliasValue string) *co
 			externalCmd.Stdin = io.In
 			preparedCmd := run.PrepareCmd(externalCmd)
 			if err = preparedCmd.Run(); err != nil {
+				var execError *exec.ExitError
+				if errors.As(err, &execError) {
+					return &ExternalCommandExitError{execError}
+				}
 				return fmt.Errorf("failed to run external command: %w\n", err)
 			}
 			return nil
diff --git a/pkg/cmd/root/extension.go b/pkg/cmd/root/extension.go
index 52ef6b0a4..d6d495103 100644
--- a/pkg/cmd/root/extension.go
+++ b/pkg/cmd/root/extension.go
@@ -1,34 +1,30 @@
 package root
 
 import (
+	"errors"
 	"fmt"
+	"os/exec"
 
-	"github.com/cli/cli/v2/git"
-	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/extensions"
 	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/spf13/cobra"
 )
 
+type ExternalCommandExitError struct {
+	*exec.ExitError
+}
+
 func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ext extensions.Extension) *cobra.Command {
-	var short string
-	if ext.IsLocal() {
-		short = fmt.Sprintf("Local extension gh-%s", ext.Name())
-	} else {
-		path := ext.URL()
-		if u, err := git.ParseURL(ext.URL()); err == nil {
-			if r, err := ghrepo.FromURL(u); err == nil {
-				path = ghrepo.FullName(r)
-			}
-		}
-		short = fmt.Sprintf("Extension %s", path)
-	}
 	return &cobra.Command{
 		Use:   ext.Name(),
-		Short: short,
+		Short: fmt.Sprintf("Extension %s", ext.Name()),
 		RunE: func(c *cobra.Command, args []string) error {
 			args = append([]string{ext.Name()}, args...)
 			if _, err := em.Dispatch(args, io.In, io.Out, io.ErrOut); err != nil {
+				var execError *exec.ExitError
+				if errors.As(err, &execError) {
+					return &ExternalCommandExitError{execError}
+				}
 				return fmt.Errorf("failed to run extension: %w\n", err)
 			}
 			return nil
diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go
index 95af0ccea..2583472c0 100644
--- a/pkg/extensions/extension.go
+++ b/pkg/extensions/extension.go
@@ -20,6 +20,7 @@ type Extension interface {
 	Path() string // Path to executable
 	URL() string
 	CurrentVersion() string
+	LatestVersion() string
 	IsPinned() bool
 	UpdateAvailable() bool
 	IsBinary() bool
diff --git a/pkg/extensions/extension_mock.go b/pkg/extensions/extension_mock.go
index a97de8f62..500603661 100644
--- a/pkg/extensions/extension_mock.go
+++ b/pkg/extensions/extension_mock.go
@@ -29,6 +29,9 @@ var _ Extension = &ExtensionMock{}
 //			IsPinnedFunc: func() bool {
 //				panic("mock out the IsPinned method")
 //			},
+//			LatestVersionFunc: func() string {
+//				panic("mock out the LatestVersion method")
+//			},
 //			NameFunc: func() string {
 //				panic("mock out the Name method")
 //			},
@@ -60,6 +63,9 @@ type ExtensionMock struct {
 	// IsPinnedFunc mocks the IsPinned method.
 	IsPinnedFunc func() bool
 
+	// LatestVersionFunc mocks the LatestVersion method.
+	LatestVersionFunc func() string
+
 	// NameFunc mocks the Name method.
 	NameFunc func() string
 
@@ -86,6 +92,9 @@ type ExtensionMock struct {
 		// IsPinned holds details about calls to the IsPinned method.
 		IsPinned []struct {
 		}
+		// LatestVersion holds details about calls to the LatestVersion method.
+		LatestVersion []struct {
+		}
 		// Name holds details about calls to the Name method.
 		Name []struct {
 		}
@@ -103,6 +112,7 @@ type ExtensionMock struct {
 	lockIsBinary        sync.RWMutex
 	lockIsLocal         sync.RWMutex
 	lockIsPinned        sync.RWMutex
+	lockLatestVersion   sync.RWMutex
 	lockName            sync.RWMutex
 	lockPath            sync.RWMutex
 	lockURL             sync.RWMutex
@@ -217,6 +227,33 @@ func (mock *ExtensionMock) IsPinnedCalls() []struct {
 	return calls
 }
 
+// LatestVersion calls LatestVersionFunc.
+func (mock *ExtensionMock) LatestVersion() string {
+	if mock.LatestVersionFunc == nil {
+		panic("ExtensionMock.LatestVersionFunc: method is nil but Extension.LatestVersion was just called")
+	}
+	callInfo := struct {
+	}{}
+	mock.lockLatestVersion.Lock()
+	mock.calls.LatestVersion = append(mock.calls.LatestVersion, callInfo)
+	mock.lockLatestVersion.Unlock()
+	return mock.LatestVersionFunc()
+}
+
+// LatestVersionCalls gets all the calls that were made to LatestVersion.
+// Check the length with:
+//
+//	len(mockedExtension.LatestVersionCalls())
+func (mock *ExtensionMock) LatestVersionCalls() []struct {
+} {
+	var calls []struct {
+	}
+	mock.lockLatestVersion.RLock()
+	calls = mock.calls.LatestVersion
+	mock.lockLatestVersion.RUnlock()
+	return calls
+}
+
 // Name calls NameFunc.
 func (mock *ExtensionMock) Name() string {
 	if mock.NameFunc == nil {


Test patch:
diff --git a/git/client_test.go b/git/client_test.go
index 6bbeee27e..5e96bb1b7 100644
--- a/git/client_test.go
+++ b/git/client_test.go
@@ -834,6 +834,84 @@ func TestClientPathFromRoot(t *testing.T) {
 	}
 }
 
+func TestClientUnsetRemoteResolution(t *testing.T) {
+	tests := []struct {
+		name          string
+		cmdExitStatus int
+		cmdStdout     string
+		cmdStderr     string
+		wantCmdArgs   string
+		wantErrorMsg  string
+	}{
+		{
+			name:        "unset remote resolution",
+			wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`,
+		},
+		{
+			name:          "git error",
+			cmdExitStatus: 1,
+			cmdStderr:     "git error message",
+			wantCmdArgs:   `path/to/git config --unset remote.origin.gh-resolved`,
+			wantErrorMsg:  "failed to run git: git error message",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
+			client := Client{
+				GitPath:        "path/to/git",
+				commandContext: cmdCtx,
+			}
+			err := client.UnsetRemoteResolution(context.Background(), "origin")
+			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
+			if tt.wantErrorMsg == "" {
+				assert.NoError(t, err)
+			} else {
+				assert.EqualError(t, err, tt.wantErrorMsg)
+			}
+		})
+	}
+}
+
+func TestClientSetRemoteBranches(t *testing.T) {
+	tests := []struct {
+		name          string
+		cmdExitStatus int
+		cmdStdout     string
+		cmdStderr     string
+		wantCmdArgs   string
+		wantErrorMsg  string
+	}{
+		{
+			name:        "set remote branches",
+			wantCmdArgs: `path/to/git remote set-branches origin trunk`,
+		},
+		{
+			name:          "git error",
+			cmdExitStatus: 1,
+			cmdStderr:     "git error message",
+			wantCmdArgs:   `path/to/git remote set-branches origin trunk`,
+			wantErrorMsg:  "failed to run git: git error message",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
+			client := Client{
+				GitPath:        "path/to/git",
+				commandContext: cmdCtx,
+			}
+			err := client.SetRemoteBranches(context.Background(), "origin", "trunk")
+			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
+			if tt.wantErrorMsg == "" {
+				assert.NoError(t, err)
+			} else {
+				assert.EqualError(t, err, tt.wantErrorMsg)
+			}
+		})
+	}
+}
+
 func TestClientFetch(t *testing.T) {
 	tests := []struct {
 		name          string
diff --git a/pkg/cmd/extension/extension_test.go b/pkg/cmd/extension/extension_test.go
index 0a50cbfe8..9b56e0bee 100644
--- a/pkg/cmd/extension/extension_test.go
+++ b/pkg/cmd/extension/extension_test.go
@@ -8,7 +8,7 @@ import (
 
 func TestUpdateAvailable_IsLocal(t *testing.T) {
 	e := &Extension{
-		isLocal: true,
+		kind: LocalKind,
 	}
 
 	assert.False(t, e.UpdateAvailable())
@@ -16,7 +16,7 @@ func TestUpdateAvailable_IsLocal(t *testing.T) {
 
 func TestUpdateAvailable_NoCurrentVersion(t *testing.T) {
 	e := &Extension{
-		isLocal: false,
+		kind: LocalKind,
 	}
 
 	assert.False(t, e.UpdateAvailable())
@@ -24,7 +24,7 @@ func TestUpdateAvailable_NoCurrentVersion(t *testing.T) {
 
 func TestUpdateAvailable_NoLatestVersion(t *testing.T) {
 	e := &Extension{
-		isLocal:        false,
+		kind:           BinaryKind,
 		currentVersion: "1.0.0",
 	}
 
@@ -33,7 +33,7 @@ func TestUpdateAvailable_NoLatestVersion(t *testing.T) {
 
 func TestUpdateAvailable_CurrentVersionIsLatestVersion(t *testing.T) {
 	e := &Extension{
-		isLocal:        false,
+		kind:           BinaryKind,
 		currentVersion: "1.0.0",
 		latestVersion:  "1.0.0",
 	}
@@ -43,7 +43,7 @@ func TestUpdateAvailable_CurrentVersionIsLatestVersion(t *testing.T) {
 
 func TestUpdateAvailable(t *testing.T) {
 	e := &Extension{
-		isLocal:        false,
+		kind:           BinaryKind,
 		currentVersion: "1.0.0",
 		latestVersion:  "1.1.0",
 	}
diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go
index b3fe64567..6d9b63124 100644
--- a/pkg/cmd/extension/manager_test.go
+++ b/pkg/cmd/extension/manager_test.go
@@ -80,12 +80,8 @@ func TestManager_List(t *testing.T) {
 	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()
+	gc.On("ForRepo", dirOne).Return(gcOne).Once()
+	gc.On("ForRepo", dirTwo).Return(gcTwo).Once()
 
 	m := newTestManager(tempDir, nil, gc, nil)
 	exts := m.List()
@@ -145,9 +141,7 @@ func TestManager_Dispatch(t *testing.T) {
 	assert.NoError(t, stubExtension(extPath))
 
 	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()
+	gc.On("ForRepo", extDir).Return(gcOne).Once()
 
 	m := newTestManager(tempDir, nil, gc, nil)
 
@@ -223,9 +217,7 @@ func TestManager_Upgrade_NoMatchingExtension(t *testing.T) {
 	assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
 	ios, _, stdout, stderr := iostreams.Test()
 	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()
+	gc.On("ForRepo", extDir).Return(gcOne).Once()
 	m := newTestManager(tempDir, nil, gc, ios)
 	err := m.Upgrade("invalid", false)
 	assert.EqualError(t, err, `no extension matched "invalid"`)
@@ -244,12 +236,8 @@ func TestManager_UpgradeExtensions(t *testing.T) {
 	assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
 	ios, _, stdout, stderr := iostreams.Test()
 	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()
+	gc.On("ForRepo", dirOne).Return(gcOne).Times(3)
+	gc.On("ForRepo", dirTwo).Return(gcTwo).Times(3)
 	gcOne.On("Remotes").Return(nil, nil).Once()
 	gcTwo.On("Remotes").Return(nil, nil).Once()
 	gcOne.On("Pull", "", "").Return(nil).Once()
@@ -286,12 +274,8 @@ func TestManager_UpgradeExtensions_DryRun(t *testing.T) {
 	assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
 	ios, _, stdout, stderr := iostreams.Test()
 	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()
+	gc.On("ForRepo", dirOne).Return(gcOne).Twice()
+	gc.On("ForRepo", dirTwo).Return(gcTwo).Twice()
 	gcOne.On("Remotes").Return(nil, nil).Once()
 	gcTwo.On("Remotes").Return(nil, nil).Once()
 	m := newTestManager(tempDir, nil, gc, ios)
@@ -355,9 +339,7 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
 	assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
 	ios, _, stdout, stderr := iostreams.Test()
 	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()
+	gc.On("ForRepo", extensionDir).Return(gcOne).Times(3)
 	gcOne.On("Remotes").Return(nil, nil).Once()
 	gcOne.On("Pull", "", "").Return(nil).Once()
 	m := newTestManager(tempDir, nil, gc, ios)
@@ -381,9 +363,7 @@ func TestManager_UpgradeExtension_GitExtension_DryRun(t *testing.T) {
 	assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
 	ios, _, stdout, stderr := iostreams.Test()
 	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()
+	gc.On("ForRepo", extDir).Return(gcOne).Twice()
 	gcOne.On("Remotes").Return(nil, nil).Once()
 	m := newTestManager(tempDir, nil, gc, ios)
 	m.EnableDryRunMode()
@@ -407,9 +387,7 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
 	assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
 	ios, _, stdout, stderr := iostreams.Test()
 	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()
+	gc.On("ForRepo", extensionDir).Return(gcOne).Times(3)
 	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()
@@ -721,9 +699,7 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) {
 	ios, _, _, _ := iostreams.Test()
 
 	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()
+	gc.On("ForRepo", extDir).Return(gcOne).Once()
 
 	m := newTestManager(tempDir, nil, gc, ios)
 
@@ -732,7 +708,8 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, 1, len(exts))
 	ext := exts[0]
-	ext.isPinned = true
+	pinnedTrue := true
+	ext.isPinned = &pinnedTrue
 	ext.latestVersion = "new version"
 
 	err = m.upgradeExtension(ext, false)
diff --git a/pkg/cmd/repo/clone/clone_test.go b/pkg/cmd/repo/clone/clone_test.go
index 98fc3736c..0ea3a65af 100644
--- a/pkg/cmd/repo/clone/clone_test.go
+++ b/pkg/cmd/repo/clone/clone_test.go
@@ -247,6 +247,7 @@ func Test_RepoClone_hasParent(t *testing.T) {
 	cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "")
 	cs.Register(`git -C REPO remote add -t trunk upstream https://github.com/hubot/ORIG.git`, 0, "")
 	cs.Register(`git -C REPO fetch upstream`, 0, "")
+	cs.Register(`git -C REPO remote set-branches upstream *`, 0, "")
 
 	_, err := runCloneCommand(httpClient, "OWNER/REPO")
 	if err != nil {
@@ -284,6 +285,7 @@ func Test_RepoClone_hasParent_upstreamRemoteName(t *testing.T) {
 	cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "")
 	cs.Register(`git -C REPO remote add -t trunk test https://github.com/hubot/ORIG.git`, 0, "")
 	cs.Register(`git -C REPO fetch test`, 0, "")
+	cs.Register(`git -C REPO remote set-branches test *`, 0, "")
 
 	_, err := runCloneCommand(httpClient, "OWNER/REPO --upstream-remote-name test")
 	if err != nil {