Repository: cli/cli
Pull request #10197 (2025-01-07 21:50:06)

Remove naked return values from `ReadBranchConfig` and `prSelectorForCurrentBranch`
## Motivation

After some discussion with @williammartin, we agreed that the naked return values pattern is something that we'd like to avoid in our codebase and that we should make an effort to remove them when we encounter them. This is the PR to remove the named return values from `ReadBranchConfig` that I encountered while working through #10188.

I also refactored `ReadBranchConfig` for two reasons: to surface the previously hidden errors it could produce and to allow for better test coverage of its core functionality. I did the latter by separating out the `git` call and the `BranchConfig` struct creation into their own functions to leverage dependency injection of the `git` call's return value into the `BranchConfig` creation.

While making this refactor work with the rest of the tool, I stumbled upon more named return values in `prSelectorForCurrentBranch` and decided to remove these as well. This resulted in a slight reorganization of the function in a way that I believe makes it easier to follow.

## Changes

- **Remove named returns from ReadBranchConfig and surface errors**
- **Refactor ReadBranchConfig for test coverage of newly returned erros**
- **Remove named return values from prSelectorForCurrentBranch**

## Testing

I haven't touched any of the original tests besides accommodating the new `ReadBranchConfig` api, and they are all passing, so I'm decently confident that this won't introduce any breaking changes. However, to exercise all the changes, follow these steps:

1. Pull down the branch `gh pr checkout 10197`
2. Build the branch
3. Execute the `gh pr status` command


Patch:
diff --git a/docs/triage.md b/docs/triage.md
index 7f8bbbd7b..9d54fcfe7 100644
--- a/docs/triage.md
+++ b/docs/triage.md
@@ -1,51 +1,27 @@
 # Triage role
 
-As we get more issues and pull requests opened on the GitHub CLI, we've decided on a weekly rotation triage role as defined by our First Responder (FR) rotation. The primary responsibility of the FR during that week is to triage incoming issues from the Open Source community, as defined below. An issue is considered "triaged" when the `needs-triage` label is removed.
+As we get more issues and pull requests opened on the GitHub CLI, we've decided on a weekly rotation
+triage role. The initial expectation is that the person in the role for the week spends no more than
+2 hours a day on this work; we can refine that as needed.
 
-## Expectations for triaging incoming issues
+## Expectations for incoming issues
 
-Review and label [open issues missing either the `enhancement`, `bug`, or `docs` label](https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+-label%3Abug%2Cenhancement%2Cdocs+) and the label(s) corresponding to the command space prefixed with `gh-`, such as `gh-pr` for the `gh pr` command set, or `gh-extension` for the `gh extension` command set. 
+Review and label [open issues missing either the `enhancement`, `bug`, or `docs` label](https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+-label%3Abug%2Cenhancement%2Cdocs+).
 
-Then, engage with the issue and community with the goal to remove the `needs-triage` label from the issue. The heuristics for triaging the different issue types are as follow:
+Any issue that is accepted to be done as either an `enhancement`, `bug`, or `docs` requires explicit Acceptance Criteria in a comment on the issue before `needs-triage` label is removed.
 
-### Bugs
+To be considered triaged, `enhancement` issues require at least one of the following additional labels:
 
-To be considered triaged, `bug` issues require the following:
+- `core`: reserved for the core CLI team
+- `help wanted`: signal that we are accepting contributions for this
+- `discuss`: add to our team's queue to discuss during a sync
+- `needs-investigation`: work that requires a mystery be solved by the core team before it can move forward
+- `needs-user-input`: we need more information from our users before the task can move forward
 
-- A severity label `p1`, `p2`, and `p3`
-- Clearly defined Acceptance Criteria, added to the Issue as a standalone comment (see [example](https://github.com/cli/cli/issues/9469#issuecomment-2292315743))
-
-#### Bug severities
-
-| Severity | Description |
-| - | - |
-| `p1` | Affects a large population and inhibits work |
-| `p2` | Affects more than a few users but doesn't prevent core functions |
-| `p3` | Affects a small number of users or is largely cosmetic |
-
-### Enhancements
-
-To be considered triaged, `enhancement` issues require either
-
-- Clearly defined Acceptance Criteria as above 
-- The `needs-investigation` or `needs-design` label with a clearly defined set of open questions to be investigated.
-
-### Docs
-
-To be considered triaged, `docs` issues require clearly defined Acceptance Criteria, as defined above 
-
-## Additional triaging processes and labels
-
-Before removing the `needs-triage` label, consider adding any of the following labels below.
-
-| Label | Description |
-| - | - |
-| `discuss` | Some issues require discussion with the internal team. Adding this label will automatically open up an internal discussion with the team to facilitate this discussion. |
-| `core` |  Defines what we would like to do internally. We tend to lean towards `help wanted` by default, and adding `core` should be reserved for trickier issues or implementations we have strong opinions/preferences about. |
-| `good first issue` | Used to denote when an issue may be a good candidate for a first-time contributor to the CLI. These are usually small and well defined issues. |
-| `help wanted` |  Defines what we feel the community could solve should they care to contribute, respectively. We tend to lean towards `help wanted` by default, and adding `core` should be reserved for trickier issues or implementations we have strong opinions/preferences about. |
-| `invalid` | Added to spam and abusive issues. |
-| `needs-user-input` | After asking any contributors for more information, add this label so it is clear that the issue has been responded to and we are waiting on the user. |
+To be considered triaged, `bug` issues require a severity label: one of `p1`, `p2`, or `p3`, which are defined as follows:
+ - `p1`: Affects a large population and inhibits work
+ - `p2`: Affects more than a few users but doesn't prevent core functions
+ - `p3`: Affects a small number of users or is largely cosmetic
 
 ## Expectations for community pull requests
 
@@ -53,13 +29,36 @@ All incoming pull requests are assigned to one of the engineers for review on a
 The person in a triage role for a week could take a glance at these pull requests, mostly to see whether
 the changeset is feasible and to allow the associated CI run for new contributors.
 
-## Spam and abuse
-
-The primary goal of triaging spam and abuse is to remove distracting and offensive content from our community.
-
-We get a lot of spam. Whenever you determine an issue as spam, add the `invalid` label and close it as "won't do". For spammy comments, simply mark them as spam using GitHub's built-in spam feature.
-
-Abusive contributions are defined by our [Code of Conduct](../.github/CODE-OF-CONDUCT.md). Any contribution you determine abusive should be removed. Repeat offenses or particularly offensive abuse should be reported using GitHub's reporting features and the user blocked. If an entire issue is abusive, label it as `invalid` and close as "won't do".
+## Issue triage flowchart
+
+- can this be closed outright?
+  - e.g. spam/junk
+  - add the `invalid` label
+  - close without comment
+- do we not want to do it?
+  - e.g. we have already discussed not wanting to do or it's a duplicate issue
+  - add the appropriate label (e.g. `duplicate`)
+  - comment and close
+- do we want external contribution for this?
+  - e.g. the task is relatively straightforward, but the core team does not have the bandwidth to take it on
+  - ensure that the thread contains all the context necessary for someone new to pick this up
+  - add the `help wanted` label
+  - consider adding `good first issue` label
+- do we want external design contribution for this?
+  - e.g. the task is worthwhile, but needs design work to flesh out the details before implementation and the core team does not have the bandwidth to take it on
+  - ensure that the thread contains all the context necessary for someone new to pick this up
+  - add both the `help wanted` and `needs-design` labels
+- do we want to do it?
+  - add the `core` label
+  - comment acknowledging that
+- is it intriguing, but requires discussion?
+  - Add the `discuss` label
+  - Add the `needs-investigation` label if engineering research is required before action can be taken
+- does it need more info from the issue author?
+  - ask the user for details
+  - add the `needs-user-input` label
+- is it a usage/support question?
+  - Convert the Issue to a Discussion
 
 ## Weekly PR audit
 
diff --git a/git/client.go b/git/client.go
index 1a6d9ae7f..19688ca51 100644
--- a/git/client.go
+++ b/git/client.go
@@ -377,19 +377,36 @@ func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte,
 }
 
 // ReadBranchConfig parses the `branch.BRANCH.(remote|merge|gh-merge-base)` part of git config.
-func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg BranchConfig) {
+// If no branch config is found or there is an error in the command, it returns an empty BranchConfig.
+// Downstream consumers of ReadBranchConfig should consider the behavior they desire if this errors,
+// as an empty config is not necessarily breaking.
+func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchConfig, error) {
+
 	prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch))
 	args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|%s)$", prefix, MergeBaseConfig)}
 	cmd, err := c.Command(ctx, args...)
 	if err != nil {
-		return
+		return BranchConfig{}, err
 	}
+
 	out, err := cmd.Output()
 	if err != nil {
-		return
+		// This is the error we expect if the git command does not run successfully.
+		// If the ExitCode is 1, then we just didn't find any config for the branch.
+		var gitError *GitError
+		if ok := errors.As(err, &gitError); ok && gitError.ExitCode != 1 {
+			return BranchConfig{}, err
+		}
+		return BranchConfig{}, nil
 	}
 
-	for _, line := range outputLines(out) {
+	return parseBranchConfig(outputLines(out)), nil
+}
+
+func parseBranchConfig(configLines []string) BranchConfig {
+	var cfg BranchConfig
+
+	for _, line := range configLines {
 		parts := strings.SplitN(line, " ", 2)
 		if len(parts) < 2 {
 			continue
@@ -412,7 +429,7 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg Branc
 			cfg.MergeBase = parts[1]
 		}
 	}
-	return
+	return cfg
 }
 
 // SetBranchConfig sets the named value on the given branch.
diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go
index 2e3e0de51..f51cde4c9 100644
--- a/pkg/cmd/issue/create/create.go
+++ b/pkg/cmd/issue/create/create.go
@@ -72,7 +72,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
 			$ gh issue create --assignee monalisa,hubot
 			$ gh issue create --assignee "@me"
 			$ gh issue create --project "Roadmap"
-			$ gh issue create --template "Bug Report"
+			$ gh issue create --template "bug_report.md"
 		`),
 		Args:    cmdutil.NoArgsQuoteReminder,
 		Aliases: []string{"new"},
@@ -129,7 +129,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
 	cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `title`")
 	cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`")
 	cmd.Flags().StringVar(&opts.RecoverFile, "recover", "", "Recover input from a failed run of create")
-	cmd.Flags().StringVarP(&opts.Template, "template", "T", "", "Template `name` to use as starting body text")
+	cmd.Flags().StringVarP(&opts.Template, "template", "T", "", "Template `file` to use as starting body text")
 
 	return cmd
 }
diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go
index db160b419..124bd4b07 100644
--- a/pkg/cmd/pr/create/create.go
+++ b/pkg/cmd/pr/create/create.go
@@ -263,17 +263,17 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
 	return cmd
 }
 
-func createRun(opts *CreateOptions) (err error) {
+func createRun(opts *CreateOptions) error {
 	ctx, err := NewCreateContext(opts)
 	if err != nil {
-		return
+		return err
 	}
 
 	client := ctx.Client
 
 	state, err := NewIssueState(*ctx, *opts)
 	if err != nil {
-		return
+		return err
 	}
 
 	var openURL string
@@ -288,15 +288,15 @@ func createRun(opts *CreateOptions) (err error) {
 		}
 		err = handlePush(*opts, *ctx)
 		if err != nil {
-			return
+			return err
 		}
 		openURL, err = generateCompareURL(*ctx, *state)
 		if err != nil {
-			return
+			return err
 		}
 		if !shared.ValidURL(openURL) {
 			err = fmt.Errorf("cannot open in browser: maximum URL length exceeded")
-			return
+			return err
 		}
 		return previewPR(*opts, openURL)
 	}
@@ -344,7 +344,7 @@ func createRun(opts *CreateOptions) (err error) {
 	if !opts.EditorMode && (opts.FillVerbose || opts.Autofill || opts.FillFirst || (opts.TitleProvided && opts.BodyProvided)) {
 		err = handlePush(*opts, *ctx)
 		if err != nil {
-			return
+			return err
 		}
 		return submitPR(*opts, *ctx, *state)
 	}
@@ -368,7 +368,7 @@ func createRun(opts *CreateOptions) (err error) {
 			var template shared.Template
 			template, err = tpl.Select(opts.Template)
 			if err != nil {
-				return
+				return err
 			}
 			if state.Title == "" {
 				state.Title = template.Title()
@@ -378,18 +378,18 @@ func createRun(opts *CreateOptions) (err error) {
 
 		state.Title, state.Body, err = opts.TitledEditSurvey(state.Title, state.Body)
 		if err != nil {
-			return
+			return err
 		}
 		if state.Title == "" {
 			err = fmt.Errorf("title can't be blank")
-			return
+			return err
 		}
 	} else {
 
 		if !opts.TitleProvided {
 			err = shared.TitleSurvey(opts.Prompter, opts.IO, state)
 			if err != nil {
-				return
+				return err
 			}
 		}
 
@@ -403,12 +403,12 @@ func createRun(opts *CreateOptions) (err error) {
 				if opts.Template != "" {
 					template, err = tpl.Select(opts.Template)
 					if err != nil {
-						return
+						return err
 					}
 				} else {
 					template, err = tpl.Choose()
 					if err != nil {
-						return
+						return err
 					}
 				}
 
@@ -419,13 +419,13 @@ func createRun(opts *CreateOptions) (err error) {
 
 			err = shared.BodySurvey(opts.Prompter, state, templateContent)
 			if err != nil {
-				return
+				return err
 			}
 		}
 
 		openURL, err = generateCompareURL(*ctx, *state)
 		if err != nil {
-			return
+			return err
 		}
 
 		allowPreview := !state.HasMetadata() && shared.ValidURL(openURL) && !opts.DryRun
@@ -444,12 +444,12 @@ func createRun(opts *CreateOptions) (err error) {
 			}
 			err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.BaseRepo, fetcher, state)
 			if err != nil {
-				return
+				return err
 			}
 
 			action, err = shared.ConfirmPRSubmission(opts.Prompter, !state.HasMetadata() && !opts.DryRun, false, state.Draft)
 			if err != nil {
-				return
+				return err
 			}
 		}
 	}
@@ -457,12 +457,12 @@ func createRun(opts *CreateOptions) (err error) {
 	if action == shared.CancelAction {
 		fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
 		err = cmdutil.CancelError
-		return
+		return err
 	}
 
 	err = handlePush(*opts, *ctx)
 	if err != nil {
-		return
+		return err
 	}
 
 	if action == shared.PreviewAction {
@@ -479,7 +479,7 @@ func createRun(opts *CreateOptions) (err error) {
 	}
 
 	err = errors.New("expected to cancel, preview, or submit")
-	return
+	return err
 }
 
 var regexPattern = regexp.MustCompile(`(?m)^`)
@@ -680,7 +680,10 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) {
 	var headRepo ghrepo.Interface
 	var headRemote *ghContext.Remote
 
-	headBranchConfig := gitClient.ReadBranchConfig(context.Background(), headBranch)
+	headBranchConfig, err := gitClient.ReadBranchConfig(context.Background(), headBranch)
+	if err != nil {
+		return nil, err
+	}
 	if isPushEnabled {
 		// determine whether the head branch is already pushed to a remote
 		if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found {
diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go
index e4a70eb22..3f036c0cd 100644
--- a/pkg/cmd/pr/shared/finder.go
+++ b/pkg/cmd/pr/shared/finder.go
@@ -37,7 +37,7 @@ type finder struct {
 	branchFn     func() (string, error)
 	remotesFn    func() (remotes.Remotes, error)
 	httpClient   func() (*http.Client, error)
-	branchConfig func(string) git.BranchConfig
+	branchConfig func(string) (git.BranchConfig, error)
 	progress     progressIndicator
 
 	repo       ghrepo.Interface
@@ -58,7 +58,7 @@ func NewFinder(factory *cmdutil.Factory) PRFinder {
 		remotesFn:  factory.Remotes,
 		httpClient: factory.HttpClient,
 		progress:   factory.IOStreams,
-		branchConfig: func(s string) git.BranchConfig {
+		branchConfig: func(s string) (git.BranchConfig, error) {
 			return factory.GitClient.ReadBranchConfig(context.Background(), s)
 		},
 	}
@@ -238,7 +238,10 @@ func (f *finder) parseCurrentBranch() (string, int, error) {
 		return "", 0, err
 	}
 
-	branchConfig := f.branchConfig(prHeadRef)
+	branchConfig, err := f.branchConfig(prHeadRef)
+	if err != nil {
+		return "", 0, err
+	}
 
 	// the branch is configured to merge a special PR head ref
 	if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
diff --git a/pkg/cmd/pr/status/status.go b/pkg/cmd/pr/status/status.go
index 27666461d..a97a59e44 100644
--- a/pkg/cmd/pr/status/status.go
+++ b/pkg/cmd/pr/status/status.go
@@ -72,6 +72,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
 }
 
 func statusRun(opts *StatusOptions) error {
+	ctx := context.Background()
 	httpClient, err := opts.HttpClient()
 	if err != nil {
 		return err
@@ -93,7 +94,11 @@ func statusRun(opts *StatusOptions) error {
 		}
 
 		remotes, _ := opts.Remotes()
-		currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(opts.GitClient, baseRepo, currentBranch, remotes)
+		branchConfig, err := opts.GitClient.ReadBranchConfig(ctx, currentBranch)
+		if err != nil {
+			return err
+		}
+		currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(branchConfig, baseRepo, currentBranch, remotes)
 		if err != nil {
 			return fmt.Errorf("could not query for pull request for current branch: %w", err)
 		}
@@ -184,31 +189,42 @@ func statusRun(opts *StatusOptions) error {
 	return nil
 }
 
-func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface, prHeadRef string, rem ghContext.Remotes) (prNumber int, selector string, err error) {
-	selector = prHeadRef
-	branchConfig := gitClient.ReadBranchConfig(context.Background(), prHeadRef)
-
+func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.Interface, prHeadRef string, rem ghContext.Remotes) (int, string, error) {
 	// the branch is configured to merge a special PR head ref
 	prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`)
 	if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
-		prNumber, _ = strconv.Atoi(m[1])
-		return
+		prNumber, err := strconv.Atoi(m[1])
+		if err != nil {
+			return 0, "", err
+		}
+		return prNumber, prHeadRef, nil
 	}
 
 	var branchOwner string
 	if branchConfig.RemoteURL != nil {
 		// the branch merges from a remote specified by URL
-		if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil {
-			branchOwner = r.RepoOwner()
+		r, err := ghrepo.FromURL(branchConfig.RemoteURL)
+		if err != nil {
+			// TODO: We aren't returning the error because we discovered that it was shadowed
+			// before refactoring to its current return pattern. Thus, we aren't confident
+			// that returning the error won't break existing behavior.
+			return 0, prHeadRef, nil
 		}
+		branchOwner = r.RepoOwner()
 	} else if branchConfig.RemoteName != "" {
 		// the branch merges from a remote specified by name
-		if r, err := rem.FindByName(branchConfig.RemoteName); err == nil {
-			branchOwner = r.RepoOwner()
+		r, err := rem.FindByName(branchConfig.RemoteName)
+		if err != nil {
+			// TODO: We aren't returning the error because we discovered that it was shadowed
+			// before refactoring to its current return pattern. Thus, we aren't confident
+			// that returning the error won't break existing behavior.
+			return 0, prHeadRef, nil
 		}
+		branchOwner = r.RepoOwner()
 	}
 
 	if branchOwner != "" {
+		selector := prHeadRef
 		if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") {
 			selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
 		}
@@ -216,9 +232,10 @@ func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface
 		if !strings.EqualFold(branchOwner, baseRepo.RepoOwner()) {
 			selector = fmt.Sprintf("%s:%s", branchOwner, selector)
 		}
+		return 0, selector, nil
 	}
 
-	return
+	return 0, prHeadRef, nil
 }
 
 func totalApprovals(pr *api.PullRequest) int {


Test patch:
diff --git a/git/client_test.go b/git/client_test.go
index fff1397f3..fe3047db9 100644
--- a/git/client_test.go
+++ b/git/client_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net/url"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -730,14 +731,35 @@ func TestClientReadBranchConfig(t *testing.T) {
 		cmdExitStatus    int
 		cmdStdout        string
 		cmdStderr        string
-		wantCmdArgs      string
+		branch           string
 		wantBranchConfig BranchConfig
+		wantError        *GitError
 	}{
 		{
 			name:             "read branch config",
+			cmdExitStatus:    0,
 			cmdStdout:        "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk",
-			wantCmdArgs:      `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|gh-merge-base)$`,
+			branch:           "trunk",
 			wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk"},
+			wantError:        nil,
+		},
+		{
+			name:             "git config runs successfully but returns no output (Exit Code 1)",
+			cmdExitStatus:    1,
+			cmdStdout:        "",
+			cmdStderr:        "",
+			branch:           "trunk",
+			wantBranchConfig: BranchConfig{},
+			wantError:        nil,
+		},
+		{
+			name:             "output error (Exit Code > 1)",
+			cmdExitStatus:    2,
+			cmdStdout:        "",
+			cmdStderr:        "git error message",
+			branch:           "trunk",
+			wantBranchConfig: BranchConfig{},
+			wantError:        &GitError{},
 		},
 	}
 	for _, tt := range tests {
@@ -747,9 +769,85 @@ func TestClientReadBranchConfig(t *testing.T) {
 				GitPath:        "path/to/git",
 				commandContext: cmdCtx,
 			}
-			branchConfig := client.ReadBranchConfig(context.Background(), "trunk")
-			assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
+			branchConfig, err := client.ReadBranchConfig(context.Background(), tt.branch)
+			wantCmdArgs := fmt.Sprintf("path/to/git config --get-regexp ^branch\\.%s\\.(remote|merge|gh-merge-base)$", tt.branch)
+			assert.Equal(t, wantCmdArgs, strings.Join(cmd.Args[3:], " "))
 			assert.Equal(t, tt.wantBranchConfig, branchConfig)
+			if tt.wantError != nil {
+				assert.ErrorAs(t, err, &tt.wantError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func Test_parseBranchConfig(t *testing.T) {
+	tests := []struct {
+		name             string
+		configLines      []string
+		wantBranchConfig BranchConfig
+	}{
+		{
+			name:        "remote branch",
+			configLines: []string{"branch.trunk.remote origin"},
+			wantBranchConfig: BranchConfig{
+				RemoteName: "origin",
+			},
+		},
+		{
+			name:        "merge ref",
+			configLines: []string{"branch.trunk.merge refs/heads/trunk"},
+			wantBranchConfig: BranchConfig{
+				MergeRef: "refs/heads/trunk",
+			},
+		},
+		{
+			name:        "merge base",
+			configLines: []string{"branch.trunk.gh-merge-base gh-merge-base"},
+			wantBranchConfig: BranchConfig{
+				MergeBase: "gh-merge-base",
+			},
+		},
+		{
+			name: "remote, merge ref, and merge base all specified",
+			configLines: []string{
+				"branch.trunk.remote origin",
+				"branch.trunk.merge refs/heads/trunk",
+				"branch.trunk.gh-merge-base gh-merge-base",
+			},
+			wantBranchConfig: BranchConfig{
+				RemoteName: "origin",
+				MergeRef:   "refs/heads/trunk",
+				MergeBase:  "gh-merge-base",
+			},
+		},
+		{
+			name: "remote URL",
+			configLines: []string{
+				"branch.Frederick888/main.remote git@github.com:Frederick888/playground.git",
+				"branch.Frederick888/main.merge refs/heads/main",
+			},
+			wantBranchConfig: BranchConfig{
+				MergeRef: "refs/heads/main",
+				RemoteURL: &url.URL{
+					Scheme: "ssh",
+					User:   url.User("git"),
+					Host:   "github.com",
+					Path:   "/Frederick888/playground.git",
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			branchConfig := parseBranchConfig(tt.configLines)
+			assert.Equal(t, tt.wantBranchConfig.RemoteName, branchConfig.RemoteName)
+			assert.Equal(t, tt.wantBranchConfig.MergeRef, branchConfig.MergeRef)
+			assert.Equal(t, tt.wantBranchConfig.MergeBase, branchConfig.MergeBase)
+			if tt.wantBranchConfig.RemoteURL != nil {
+				assert.Equal(t, tt.wantBranchConfig.RemoteURL.String(), branchConfig.RemoteURL.String())
+			}
 		})
 	}
 }
diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go
index 6c0ff11e2..b8f5daf66 100644
--- a/pkg/cmd/pr/create/create_test.go
+++ b/pkg/cmd/pr/create/create_test.go
@@ -1,7 +1,6 @@
 package create
 
 import (
-	ctx "context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -1626,6 +1625,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
 	tests := []struct {
 		name                string
 		cmdStubs            func(*run.CommandStubber)
+		headBranchConfig    git.BranchConfig
 		remotes             context.Remotes
 		expectedTrackingRef trackingRef
 		expectedFound       bool
@@ -1633,18 +1633,18 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
 		{
 			name: "empty",
 			cmdStubs: func(cs *run.CommandStubber) {
-				cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
 				cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD")
 			},
+			headBranchConfig:    git.BranchConfig{},
 			expectedTrackingRef: trackingRef{},
 			expectedFound:       false,
 		},
 		{
 			name: "no match",
 			cmdStubs: func(cs *run.CommandStubber) {
-				cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
 				cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature")
 			},
+			headBranchConfig: git.BranchConfig{},
 			remotes: context.Remotes{
 				&context.Remote{
 					Remote: &git.Remote{Name: "upstream"},
@@ -1661,13 +1661,13 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
 		{
 			name: "match",
 			cmdStubs: func(cs *run.CommandStubber) {
-				cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
 				cs.Register(`git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature$`, 0, heredoc.Doc(`
 		deadbeef HEAD
 		deadb00f refs/remotes/upstream/feature
 		deadbeef refs/remotes/origin/feature
 	`))
 			},
+			headBranchConfig: git.BranchConfig{},
 			remotes: context.Remotes{
 				&context.Remote{
 					Remote: &git.Remote{Name: "upstream"},
@@ -1687,15 +1687,15 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
 		{
 			name: "respect tracking config",
 			cmdStubs: func(cs *run.CommandStubber) {
-				cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, heredoc.Doc(`
-		branch.feature.remote origin
-		branch.feature.merge refs/heads/great-feat
-	`))
 				cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(`
 		deadbeef HEAD
 		deadb00f refs/remotes/origin/feature
 	`))
 			},
+			headBranchConfig: git.BranchConfig{
+				RemoteName: "origin",
+				MergeRef:   "refs/heads/great-feat",
+			},
 			remotes: context.Remotes{
 				&context.Remote{
 					Remote: &git.Remote{Name: "origin"},
@@ -1717,8 +1717,8 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
 				GhPath:  "some/path/gh",
 				GitPath: "some/path/git",
 			}
-			headBranchConfig := gitClient.ReadBranchConfig(ctx.Background(), "feature")
-			ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", headBranchConfig)
+
+			ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", tt.headBranchConfig)
 
 			assert.Equal(t, tt.expectedTrackingRef, ref)
 			assert.Equal(t, tt.expectedFound, found)
diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go
index 258ef01e8..dd96e684a 100644
--- a/pkg/cmd/pr/shared/finder_test.go
+++ b/pkg/cmd/pr/shared/finder_test.go
@@ -10,19 +10,21 @@ import (
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/httpmock"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
+type args struct {
+	baseRepoFn   func() (ghrepo.Interface, error)
+	branchFn     func() (string, error)
+	branchConfig func(string) (git.BranchConfig, error)
+	remotesFn    func() (context.Remotes, error)
+	selector     string
+	fields       []string
+	baseBranch   string
+}
+
 func TestFind(t *testing.T) {
-	type args struct {
-		baseRepoFn   func() (ghrepo.Interface, error)
-		branchFn     func() (string, error)
-		branchConfig func(string) git.BranchConfig
-		remotesFn    func() (context.Remotes, error)
-		selector     string
-		fields       []string
-		baseBranch   string
-	}
 	tests := []struct {
 		name     string
 		args     args
@@ -231,9 +233,7 @@ func TestFind(t *testing.T) {
 				branchFn: func() (string, error) {
 					return "blueberries", nil
 				},
-				branchConfig: func(branch string) (c git.BranchConfig) {
-					return
-				},
+				branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
 			},
 			httpStub: func(r *httpmock.Registry) {
 				r.Register(
@@ -265,9 +265,7 @@ func TestFind(t *testing.T) {
 				branchFn: func() (string, error) {
 					return "blueberries", nil
 				},
-				branchConfig: func(branch string) (c git.BranchConfig) {
-					return
-				},
+				branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
 			},
 			httpStub: func(r *httpmock.Registry) {
 				r.Register(
@@ -315,11 +313,10 @@ func TestFind(t *testing.T) {
 				branchFn: func() (string, error) {
 					return "blueberries", nil
 				},
-				branchConfig: func(branch string) (c git.BranchConfig) {
-					c.MergeRef = "refs/heads/blue-upstream-berries"
-					c.RemoteName = "origin"
-					return
-				},
+				branchConfig: stubBranchConfig(git.BranchConfig{
+					MergeRef:   "refs/heads/blue-upstream-berries",
+					RemoteName: "origin",
+				}, nil),
 				remotesFn: func() (context.Remotes, error) {
 					return context.Remotes{{
 						Remote: &git.Remote{Name: "origin"},
@@ -357,11 +354,12 @@ func TestFind(t *testing.T) {
 				branchFn: func() (string, error) {
 					return "blueberries", nil
 				},
-				branchConfig: func(branch string) (c git.BranchConfig) {
+				branchConfig: func(branch string) (git.BranchConfig, error) {
 					u, _ := url.Parse("https://github.com/UPSTREAMOWNER/REPO")
-					c.MergeRef = "refs/heads/blue-upstream-berries"
-					c.RemoteURL = u
-					return
+					return stubBranchConfig(git.BranchConfig{
+						MergeRef:  "refs/heads/blue-upstream-berries",
+						RemoteURL: u,
+					}, nil)(branch)
 				},
 				remotesFn: nil,
 			},
@@ -395,10 +393,9 @@ func TestFind(t *testing.T) {
 				branchFn: func() (string, error) {
 					return "blueberries", nil
 				},
-				branchConfig: func(branch string) (c git.BranchConfig) {
-					c.RemoteName = "origin"
-					return
-				},
+				branchConfig: stubBranchConfig(git.BranchConfig{
+					RemoteName: "origin",
+				}, nil),
 				remotesFn: func() (context.Remotes, error) {
 					return context.Remotes{{
 						Remote: &git.Remote{Name: "origin"},
@@ -436,10 +433,9 @@ func TestFind(t *testing.T) {
 				branchFn: func() (string, error) {
 					return "blueberries", nil
 				},
-				branchConfig: func(branch string) (c git.BranchConfig) {
-					c.MergeRef = "refs/pull/13/head"
-					return
-				},
+				branchConfig: stubBranchConfig(git.BranchConfig{
+					MergeRef: "refs/pull/13/head",
+				}, nil),
 			},
 			httpStub: func(r *httpmock.Registry) {
 				r.Register(
@@ -462,10 +458,9 @@ func TestFind(t *testing.T) {
 				branchFn: func() (string, error) {
 					return "blueberries", nil
 				},
-				branchConfig: func(branch string) (c git.BranchConfig) {
-					c.MergeRef = "refs/pull/13/head"
-					return
-				},
+				branchConfig: stubBranchConfig(git.BranchConfig{
+					MergeRef: "refs/pull/13/head",
+				}, nil),
 			},
 			httpStub: func(r *httpmock.Registry) {
 				r.Register(
@@ -561,3 +556,49 @@ func TestFind(t *testing.T) {
 		})
 	}
 }
+
+func Test_parseCurrentBranch(t *testing.T) {
+	tests := []struct {
+		name         string
+		args         args
+		wantSelector string
+		wantPR       int
+		wantError    error
+	}{
+		{
+			name: "failed branch config",
+			args: args{
+				branchConfig: stubBranchConfig(git.BranchConfig{}, errors.New("branchConfigErr")),
+				branchFn: func() (string, error) {
+					return "blueberries", nil
+				},
+			},
+			wantSelector: "",
+			wantPR:       0,
+			wantError:    errors.New("branchConfigErr"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			f := finder{
+				httpClient: func() (*http.Client, error) {
+					return &http.Client{}, nil
+				},
+				baseRepoFn:   tt.args.baseRepoFn,
+				branchFn:     tt.args.branchFn,
+				branchConfig: tt.args.branchConfig,
+				remotesFn:    tt.args.remotesFn,
+			}
+			selector, pr, err := f.parseCurrentBranch()
+			assert.Equal(t, tt.wantSelector, selector)
+			assert.Equal(t, tt.wantPR, pr)
+			assert.Equal(t, tt.wantError, err)
+		})
+	}
+}
+
+func stubBranchConfig(branchConfig git.BranchConfig, err error) func(string) (git.BranchConfig, error) {
+	return func(branch string) (git.BranchConfig, error) {
+		return branchConfig, err
+	}
+}
diff --git a/pkg/cmd/pr/status/status_test.go b/pkg/cmd/pr/status/status_test.go
index e8e487559..43cce5f4c 100644
--- a/pkg/cmd/pr/status/status_test.go
+++ b/pkg/cmd/pr/status/status_test.go
@@ -4,11 +4,11 @@ import (
 	"bytes"
 	"io"
 	"net/http"
+	"net/url"
 	"regexp"
 	"strings"
 	"testing"
 
-	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/context"
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/config"
@@ -21,6 +21,7 @@ import (
 	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/cli/cli/v2/test"
 	"github.com/google/shlex"
+	"github.com/stretchr/testify/assert"
 )
 
 func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
@@ -95,6 +96,11 @@ func TestPRStatus(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatus.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -120,6 +126,11 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
 	// status,conclusion matches the old StatusContextRollup query
 	http.Register(httpmock.GraphQL(`status,conclusion`), httpmock.FileResponse("./fixtures/prStatusChecks.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -145,6 +156,11 @@ func TestPRStatus_reviewsAndChecksWithStatesByCount(t *testing.T) {
 	// checkRunCount,checkRunCountsByState matches the new StatusContextRollup query
 	http.Register(httpmock.GraphQL(`checkRunCount,checkRunCountsByState`), httpmock.FileResponse("./fixtures/prStatusChecksWithStatesByCount.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommandWithDetector(http, "blueberries", true, "", &fd.EnabledDetectorMock{})
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -169,6 +185,11 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranch.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -197,6 +218,11 @@ func TestPRStatus_currentBranch_defaultBranch(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranch.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -231,6 +257,11 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchClosed.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -248,6 +279,11 @@ func TestPRStatus_currentBranch_Closed_defaultBranch(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchClosedOnDefaultBranch.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -265,6 +301,11 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchMerged.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -282,6 +323,11 @@ func TestPRStatus_currentBranch_Merged_defaultBranch(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchMergedOnDefaultBranch.json"))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -299,6 +345,11 @@ func TestPRStatus_blankSlate(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.StringResponse(`{"data": {}}`))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "blueberries", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -352,6 +403,11 @@ func TestPRStatus_detachedHead(t *testing.T) {
 	defer http.Verify(t)
 	http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.StringResponse(`{"data": {}}`))
 
+	// stub successful git command
+	rs, cleanup := run.Stub()
+	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
+
 	output, err := runCommand(http, "", true, "")
 	if err != nil {
 		t.Errorf("error running command `pr status`: %v", err)
@@ -375,31 +431,219 @@ Requesting a code review from you
 	}
 }
 
-func Test_prSelectorForCurrentBranch(t *testing.T) {
+func TestPRStatus_error_ReadBranchConfig(t *testing.T) {
 	rs, cleanup := run.Stub()
 	defer cleanup(t)
+	rs.Register(`git config --get-regexp \^branch\\.`, 1, "")
 
-	rs.Register(`git config --get-regexp \^branch\\.`, 0, heredoc.Doc(`
-		branch.Frederick888/main.remote git@github.com:Frederick888/playground.git
-		branch.Frederick888/main.merge refs/heads/main
-	`))
+	_, err := runCommand(initFakeHTTP(), "blueberries", true, "")
+	assert.Error(t, err)
+}
 
-	repo := ghrepo.NewWithHost("octocat", "playground", "github.com")
-	rem := context.Remotes{
-		&context.Remote{
-			Remote: &git.Remote{Name: "origin"},
-			Repo:   repo,
+func Test_prSelectorForCurrentBranch(t *testing.T) {
+	tests := []struct {
+		name         string
+		branchConfig git.BranchConfig
+		baseRepo     ghrepo.Interface
+		prHeadRef    string
+		remotes      context.Remotes
+		wantPrNumber int
+		wantSelector string
+		wantError    error
+	}{
+		{
+			name:         "Empty branch config",
+			branchConfig: git.BranchConfig{},
+			prHeadRef:    "monalisa/main",
+			wantPrNumber: 0,
+			wantSelector: "monalisa/main",
+			wantError:    nil,
+		},
+		{
+			name: "The branch is configured to merge a special PR head ref",
+			branchConfig: git.BranchConfig{
+				MergeRef: "refs/pull/42/head",
+			},
+			prHeadRef:    "monalisa/main",
+			wantPrNumber: 42,
+			wantSelector: "monalisa/main",
+			wantError:    nil,
+		},
+		{
+			name: "Branch merges from a remote specified by URL",
+			branchConfig: git.BranchConfig{
+				RemoteURL: &url.URL{
+					Scheme: "ssh",
+					User:   url.User("git"),
+					Host:   "github.com",
+					Path:   "monalisa/playground.git",
+				},
+			},
+			baseRepo:  ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+			prHeadRef: "monalisa/main",
+			remotes: context.Remotes{
+				&context.Remote{
+					Remote: &git.Remote{Name: "origin"},
+					Repo:   ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+				},
+			},
+			wantPrNumber: 0,
+			wantSelector: "monalisa/main",
+			wantError:    nil,
+		},
+		{
+			name: "Branch merges from a remote specified by name",
+			branchConfig: git.BranchConfig{
+				RemoteName: "upstream",
+			},
+			baseRepo:  ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+			prHeadRef: "monalisa/main",
+			remotes: context.Remotes{
+				&context.Remote{
+					Remote: &git.Remote{Name: "origin"},
+					Repo:   ghrepo.NewWithHost("forkName", "playground", "github.com"),
+				},
+				&context.Remote{
+					Remote: &git.Remote{Name: "upstream"},
+					Repo:   ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+				},
+			},
+			wantPrNumber: 0,
+			wantSelector: "monalisa/main",
+			wantError:    nil,
+		},
+		{
+			name: "Branch is a fork and merges from a remote specified by URL",
+			branchConfig: git.BranchConfig{
+				RemoteURL: &url.URL{
+					Scheme: "ssh",
+					User:   url.User("git"),
+					Host:   "github.com",
+					Path:   "forkName/playground.git",
+				},
+				MergeRef: "refs/heads/main",
+			},
+			baseRepo:  ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+			prHeadRef: "monalisa/main",
+			remotes: context.Remotes{
+				&context.Remote{
+					Remote: &git.Remote{Name: "origin"},
+					Repo:   ghrepo.NewWithHost("forkName", "playground", "github.com"),
+				},
+			},
+			wantPrNumber: 0,
+			wantSelector: "forkName:main",
+			wantError:    nil,
+		},
+		{
+			name: "Branch is a fork and merges from a remote specified by name",
+			branchConfig: git.BranchConfig{
+				RemoteName: "origin",
+			},
+			baseRepo:  ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+			prHeadRef: "monalisa/main",
+			remotes: context.Remotes{
+				&context.Remote{
+					Remote: &git.Remote{Name: "origin"},
+					Repo:   ghrepo.NewWithHost("forkName", "playground", "github.com"),
+				},
+				&context.Remote{
+					Remote: &git.Remote{Name: "upstream"},
+					Repo:   ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+				},
+			},
+			wantPrNumber: 0,
+			wantSelector: "forkName:monalisa/main",
+			wantError:    nil,
+		},
+		{
+			name: "Branch specifies a mergeRef and merges from a remote specified by name",
+			branchConfig: git.BranchConfig{
+				RemoteName: "upstream",
+				MergeRef:   "refs/heads/main",
+			},
+			baseRepo:  ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+			prHeadRef: "monalisa/main",
+			remotes: context.Remotes{
+				&context.Remote{
+					Remote: &git.Remote{Name: "origin"},
+					Repo:   ghrepo.NewWithHost("forkName", "playground", "github.com"),
+				},
+				&context.Remote{
+					Remote: &git.Remote{Name: "upstream"},
+					Repo:   ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+				},
+			},
+			wantPrNumber: 0,
+			wantSelector: "main",
+			wantError:    nil,
+		},
+		{
+			name: "Branch is a fork, specifies a mergeRef, and merges from a remote specified by name",
+			branchConfig: git.BranchConfig{
+				RemoteName: "origin",
+				MergeRef:   "refs/heads/main",
+			},
+			baseRepo:  ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+			prHeadRef: "monalisa/main",
+			remotes: context.Remotes{
+				&context.Remote{
+					Remote: &git.Remote{Name: "origin"},
+					Repo:   ghrepo.NewWithHost("forkName", "playground", "github.com"),
+				},
+				&context.Remote{
+					Remote: &git.Remote{Name: "upstream"},
+					Repo:   ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+				},
+			},
+			wantPrNumber: 0,
+			wantSelector: "forkName:main",
+			wantError:    nil,
+		},
+		{
+			name: "Remote URL errors",
+			branchConfig: git.BranchConfig{
+				RemoteURL: &url.URL{
+					Scheme: "ssh",
+					User:   url.User("git"),
+					Host:   "github.com",
+					Path:   "/\\invalid?Path/",
+				},
+			},
+			prHeadRef:    "monalisa/main",
+			wantPrNumber: 0,
+			wantSelector: "monalisa/main",
+			wantError:    nil,
+		},
+		{
+			name: "Remote Name errors",
+			branchConfig: git.BranchConfig{
+				RemoteName: "nonexistentRemote",
+			},
+			prHeadRef: "monalisa/main",
+			remotes: context.Remotes{
+				&context.Remote{
+					Remote: &git.Remote{Name: "origin"},
+					Repo:   ghrepo.NewWithHost("forkName", "playground", "github.com"),
+				},
+				&context.Remote{
+					Remote: &git.Remote{Name: "upstream"},
+					Repo:   ghrepo.NewWithHost("monalisa", "playground", "github.com"),
+				},
+			},
+			wantPrNumber: 0,
+			wantSelector: "monalisa/main",
+			wantError:    nil,
 		},
 	}
-	gitClient := &git.Client{GitPath: "some/path/git"}
-	prNum, headRef, err := prSelectorForCurrentBranch(gitClient, repo, "Frederick888/main", rem)
-	if err != nil {
-		t.Fatalf("prSelectorForCurrentBranch error: %v", err)
-	}
-	if prNum != 0 {
-		t.Errorf("expected prNum to be 0, got %q", prNum)
-	}
-	if headRef != "Frederick888:main" {
-		t.Errorf("expected headRef to be \"Frederick888:main\", got %q", headRef)
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			prNum, headRef, err := prSelectorForCurrentBranch(tt.branchConfig, tt.baseRepo, tt.prHeadRef, tt.remotes)
+			assert.Equal(t, tt.wantPrNumber, prNum)
+			assert.Equal(t, tt.wantSelector, headRef)
+			assert.Equal(t, tt.wantError, err)
+		})
 	}
 }