From a8d01c70cddb8165bfe2d654e9de602fbaf6b0cc Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:42:06 -0700 Subject: [PATCH] feat: support `@{push}` revision syntax --- ...pr-create-respects-branch-pushremote.txtar | 43 ++++++ .../pr-create-respects-push-destination.txtar | 48 ++++++ ...r-create-respects-remote-pushdefault.txtar | 43 ++++++ ...r-create-respects-simple-pushdefault.txtar | 45 ++++++ pkg/cmd/pr/create/create.go | 121 ++++++--------- pkg/cmd/pr/create/create_test.go | 138 ++++-------------- 6 files changed, 257 insertions(+), 181 deletions(-) create mode 100644 acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar create mode 100644 acceptance/testdata/pr/pr-create-respects-push-destination.txtar create mode 100644 acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar create mode 100644 acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar diff --git a/acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar b/acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar new file mode 100644 index 000000000..6f0413a68 --- /dev/null +++ b/acceptance/testdata/pr/pr-create-respects-branch-pushremote.txtar @@ -0,0 +1,43 @@ +# skip 'it creates a fork owned by the user running the test' + +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Get the current username for the fork owner +exec gh api user --jq .login +stdout2env USER + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository. This will be owned by USER. +exec gh repo fork ${ORG}/${REPO} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${USER}/${FORK} +sleep 5 +exec gh repo view ${USER}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${USER}/${FORK} +cd ${FORK} + +# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork +exec git checkout -b feature-branch upstream/main +exec git config branch.feature-branch.pushRemote origin +exec git commit --allow-empty -m 'Empty Commit' +exec git push + +# Create the PR spanning upstream and fork repositories +exec gh pr create --title 'Feature Title' --body 'Feature Body' +stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1 \ No newline at end of file diff --git a/acceptance/testdata/pr/pr-create-respects-push-destination.txtar b/acceptance/testdata/pr/pr-create-respects-push-destination.txtar new file mode 100644 index 000000000..6dd7e6e4f --- /dev/null +++ b/acceptance/testdata/pr/pr-create-respects-push-destination.txtar @@ -0,0 +1,48 @@ +# skip 'it creates a fork owned by the user running the test' + +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Get the current username for the fork owner +exec gh api user --jq .login +stdout2env USER + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository. This will be owned by USER. +exec gh repo fork ${ORG}/${REPO} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${USER}/${FORK} +sleep 5 +exec gh repo view ${USER}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${USER}/${FORK} +cd ${FORK} + +# Configure default push behavior so local and remote branches will be the same +exec git config push.default current + +# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name +exec git checkout -b feature-branch +exec git branch --set-upstream-to origin/main +exec git rev-parse --abbrev-ref feature-branch@{upstream} +stdout origin/main + +# Create the PR +exec git commit --allow-empty -m 'Empty Commit' +exec git push +exec gh pr create --title 'Feature Title' --body 'Feature Body' +stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1 \ No newline at end of file diff --git a/acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar b/acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar new file mode 100644 index 000000000..cf38d3c9f --- /dev/null +++ b/acceptance/testdata/pr/pr-create-respects-remote-pushdefault.txtar @@ -0,0 +1,43 @@ +# skip 'it creates a fork owned by the user running the test' + +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Get the current username for the fork owner +exec gh api user --jq .login +stdout2env USER + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository. This will be owned by USER. +exec gh repo fork ${ORG}/${REPO} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${USER}/${FORK} +sleep 5 +exec gh repo view ${USER}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${USER}/${FORK} +cd ${FORK} + +# Prepare a branch where changes are pulled from the upstream default branch but pushed to fork +exec git checkout -b feature-branch upstream/main +exec git config remote.pushDefault origin +exec git commit --allow-empty -m 'Empty Commit' +exec git push + +# Create the PR spanning upstream and fork repositories +exec gh pr create --title 'Feature Title' --body 'Feature Body' +stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1 diff --git a/acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar b/acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar new file mode 100644 index 000000000..62d7e700e --- /dev/null +++ b/acceptance/testdata/pr/pr-create-respects-simple-pushdefault.txtar @@ -0,0 +1,45 @@ +# skip 'it creates a fork owned by the user running the test' + +# Setup environment variables used for testscript +env REPO=${SCRIPT_NAME}-${RANDOM_STRING} +env FORK=${REPO}-fork + +# Use gh as a credential helper +exec gh auth setup-git + +# Get the current username for the fork owner +exec gh api user --jq .login +stdout2env USER + +# Create a repository to act as upstream with a file so it has a default branch +exec gh repo create ${ORG}/${REPO} --add-readme --private + +# Defer repo cleanup of upstream +defer gh repo delete --yes ${ORG}/${REPO} +exec gh repo view ${ORG}/${REPO} --json id --jq '.id' +stdout2env REPO_ID + +# Create a user fork of repository. This will be owned by USER. +exec gh repo fork ${ORG}/${REPO} --fork-name ${FORK} + +# Defer repo cleanup of fork +defer gh repo delete --yes ${USER}/${FORK} +sleep 5 +exec gh repo view ${USER}/${FORK} --json id --jq '.id' +stdout2env FORK_ID + +# Clone the repo +exec gh repo clone ${USER}/${FORK} +cd ${FORK} + +# Configure default push behavior so local and remote branches have to be the same +exec git config push.default simple + +# Prepare a branch where changes are pulled from the default branch instead of remote branch of same name +exec git checkout -b feature-branch origin/main + +# Create the PR +exec git commit --allow-empty -m 'Empty Commit' +exec git push origin feature-branch +exec gh pr create --title 'Feature Title' --body 'Feature Body' +stdout https://${GH_HOST}/${ORG}/${REPO}/pull/1 diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 5f8979c11..56f7fa8d1 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -518,81 +518,34 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, u return nil } -// TODO: Replace with the finder's PullRequestRefs struct -// trackingRef represents a ref for a remote tracking branch. -type trackingRef struct { - remoteName string - branchName string -} - -func (r trackingRef) String() string { - return "refs/remotes/" + r.remoteName + "/" + r.branchName -} - -func mustParseTrackingRef(text string) trackingRef { - parts := strings.SplitN(string(text), "/", 4) - // The only place this is called is tryDetermineTrackingRef, where we are reconstructing - // the same tracking ref we passed in. If it doesn't match the expected format, this is a - // programmer error we want to know about, so it's ok to panic. - if len(parts) != 4 { - panic(fmt.Errorf("tracking ref should have four parts: %s", text)) - } - if parts[0] != "refs" || parts[1] != "remotes" { - panic(fmt.Errorf("tracking ref should start with refs/remotes/: %s", text)) +// isRemoteHeadCurrent returns true if the remote head is on the same sha as the local head. +// This is used to determine if we might need to push the local head branch to the remote. +func isRemoteHeadCurrent(gitClient *git.Client, prRefs shared.PullRequestRefs, remotes ghContext.Remotes) bool { + headRemote, err := remotes.FindByRepo(prRefs.HeadRepo.RepoOwner(), prRefs.HeadRepo.RepoName()) + if err != nil { + return false } - return trackingRef{ - remoteName: parts[2], - branchName: parts[3], - } -} - -// tryDetermineTrackingRef is intended to try and find a remote branch on the same commit as the currently checked out -// HEAD, i.e. the local branch. If there are multiple branches that might match, the first remote is chosen, which in -// practice is determined by the sorting algorithm applied much earlier in the process, roughly "upstream", "github", "origin", -// and then everything else unstably sorted. -func tryDetermineTrackingRef(gitClient *git.Client, remotes ghContext.Remotes, localBranchName string, headBranchConfig git.BranchConfig) (trackingRef, bool) { - // To try and determine the tracking ref for a local branch, we first construct a collection of refs - // that might be tracking, given the current branch's config, and the list of known remotes. - refsForLookup := []string{"HEAD"} - if headBranchConfig.RemoteName != "" && headBranchConfig.MergeRef != "" { - tr := trackingRef{ - remoteName: headBranchConfig.RemoteName, - branchName: strings.TrimPrefix(headBranchConfig.MergeRef, "refs/heads/"), - } - refsForLookup = append(refsForLookup, tr.String()) + refsForLookup := []string{"HEAD", fmt.Sprintf("refs/remotes/%s/%s", headRemote, prRefs.BranchName)} + resolvedRefs, err := gitClient.ShowRefs(context.Background(), refsForLookup) + if err != nil { + return false } - for _, remote := range remotes { - tr := trackingRef{ - remoteName: remote.Name, - branchName: localBranchName, - } - refsForLookup = append(refsForLookup, tr.String()) - } - - // Then we ask git for details about these refs, for example, refs/remotes/origin/trunk might return a hash - // for the remote tracking branch, trunk, for the remote, origin. If there is no ref, the git client returns - // no ref information. - // - // We also first check for the HEAD ref, so that we have the hash of the currently checked out commit. - resolvedRefs, _ := gitClient.ShowRefs(context.Background(), refsForLookup) - - // If there is more than one resolved ref, that means that at least one ref was found in addition to the HEAD. + // If there is more than one resolved ref, then remote head ref was resolved. if len(resolvedRefs) > 1 { headRef := resolvedRefs[0] for _, r := range resolvedRefs[1:] { - // If the hash of the remote ref doesn't match the hash of HEAD then the remote branch is not in the same - // state, so it can't be used. + // If the head ref is not the same as the remote head ref, then the remote head is not current. if r.Hash != headRef.Hash { continue } - // Otherwise we can parse the returned ref into a tracking ref and return that - return mustParseTrackingRef(r.Name), true + + return true } } - return trackingRef{}, false + return false } func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadataState, error) { @@ -628,6 +581,7 @@ func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadata } func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { + ctx := context.Background() httpClient, err := opts.HttpClient() if err != nil { return nil, err @@ -662,6 +616,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { isPushEnabled := false headBranch := opts.HeadBranch headBranchLabel := opts.HeadBranch + if headBranch == "" { headBranch, err = opts.Branch() if err != nil { @@ -686,18 +641,38 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { return nil, err } if isPushEnabled { - // TODO: This doesn't respect the @{push} revision resolution or triagular workflows assembled with - // remote.pushDefault, or branch..pushremote config settings. The finder's ParsePRRefs - // may be able to replace this function entirely. - if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found { + // Suppressing these errors as we have other means of computing the PullRequestRefs when these fail. + parsedPushRevision, _ := opts.GitClient.ParsePushRevision(ctx, headBranch) + + remotePushDefault, err := opts.GitClient.RemotePushDefault(ctx) + if err != nil { + return nil, err + } + + pushDefault, err := opts.GitClient.PushDefault(ctx) + if err != nil { + return nil, err + } + + prRefs, err := shared.ParsePRRefs(headBranch, headBranchConfig, parsedPushRevision, pushDefault, remotePushDefault, baseRepo, remotes) + if err != nil { + return nil, err + } + + remoteHeadCurrent := isRemoteHeadCurrent(gitClient, prRefs, remotes) + // If the remote head is up-to-date, and we have the headRef, we do not need to push anything. + if remoteHeadCurrent && prRefs.HeadRepo != nil && prRefs.BranchName != "" { isPushEnabled = false - if r, err := remotes.FindByName(trackingRef.remoteName); err == nil { - headRepo = r - headRemote = r - headBranchLabel = trackingRef.branchName - if !ghrepo.IsSame(baseRepo, headRepo) { - headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), trackingRef.branchName) - } + headRepo = prRefs.HeadRepo + headRemote, err = remotes.FindByRepo(headRepo.RepoOwner(), headRepo.RepoName()) + // TODO: KW what does an err here mean? + if err != nil { + return nil, err + } + + headBranchLabel = prRefs.BranchName + if !ghrepo.IsSame(baseRepo, headRepo) { + headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), prRefs.BranchName) } } } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 55012d7dd..849fa4219 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -637,6 +637,9 @@ func Test_createRun(t *testing.T) { }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/feature") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -700,6 +703,9 @@ func Test_createRun(t *testing.T) { }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/feature") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -746,6 +752,9 @@ func Test_createRun(t *testing.T) { }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/feature") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -795,6 +804,9 @@ func Test_createRun(t *testing.T) { }, cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/feature") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") cs.Register("git remote rename origin upstream", 0, "") cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") @@ -854,9 +866,12 @@ func Test_createRun(t *testing.T) { }, cmdStubs: func(cs *run.CommandStubber) { cs.Register("git show-ref --verify", 0, heredoc.Doc(` - deadbeef HEAD - deadb00f refs/remotes/upstream/feature - deadbeef refs/remotes/origin/feature`)) // determineTrackingBranch + deadbeef HEAD + deadb00f refs/remotes/upstream/feature + deadbeef refs/remotes/origin/feature`)) + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/feature") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n", @@ -894,6 +909,9 @@ func Test_createRun(t *testing.T) { deadbeef HEAD deadbeef refs/remotes/origin/my-feat2 `)) // determineTrackingBranch + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/my-feat2") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n", @@ -1075,6 +1093,9 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/feature") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1107,6 +1128,9 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") + cs.Register("git rev-parse --abbrev-ref feature@{push}", 0, "origin/feature") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1524,6 +1548,9 @@ func Test_createRun(t *testing.T) { deadbeef HEAD deadb00f refs/remotes/upstream/feature/feat2 deadbeef refs/remotes/origin/task1`)) // determineTrackingBranch + cs.Register("git rev-parse --abbrev-ref task1@{push}", 0, "origin/task1") + cs.Register("git config remote.pushDefault", 0, "") + cs.Register("git config push.default", 0, "") }, expectedOut: "https://github.com/OWNER/REPO/pull/12\n", expectedErrOut: "\nCreating pull request for monalisa:task1 into feature/feat2 in OWNER/REPO\n\n", @@ -1622,111 +1649,6 @@ func Test_createRun(t *testing.T) { } } -func Test_tryDetermineTrackingRef(t *testing.T) { - tests := []struct { - name string - cmdStubs func(*run.CommandStubber) - headBranchConfig git.BranchConfig - remotes context.Remotes - expectedTrackingRef trackingRef - expectedFound bool - }{ - { - name: "empty", - cmdStubs: func(cs *run.CommandStubber) { - 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 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"}, - Repo: ghrepo.New("octocat", "Spoon-Knife"), - }, - &context.Remote{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("hubot", "Spoon-Knife"), - }, - }, - expectedTrackingRef: trackingRef{}, - expectedFound: false, - }, - { - name: "match", - cmdStubs: func(cs *run.CommandStubber) { - 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"}, - Repo: ghrepo.New("octocat", "Spoon-Knife"), - }, - &context.Remote{ - Remote: &git.Remote{Name: "origin"}, - Repo: ghrepo.New("hubot", "Spoon-Knife"), - }, - }, - expectedTrackingRef: trackingRef{ - remoteName: "origin", - branchName: "feature", - }, - expectedFound: true, - }, - { - name: "respect tracking config", - cmdStubs: func(cs *run.CommandStubber) { - 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"}, - Repo: ghrepo.New("hubot", "Spoon-Knife"), - }, - }, - expectedTrackingRef: trackingRef{}, - expectedFound: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cs, cmdTeardown := run.Stub() - defer cmdTeardown(t) - - tt.cmdStubs(cs) - - gitClient := &git.Client{ - GhPath: "some/path/gh", - GitPath: "some/path/git", - } - - ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", tt.headBranchConfig) - - assert.Equal(t, tt.expectedTrackingRef, ref) - assert.Equal(t, tt.expectedFound, found) - }) - } -} - func Test_generateCompareURL(t *testing.T) { tests := []struct { name string