Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@nilvng
Copy link
Contributor

@nilvng nilvng commented Nov 3, 2024

fixes #2329

Acceptance criteria

If no argument specified for pr checkout and we're attached to a TTY,

  • fetch open PRs and
  • offer them in a prompter.Select

Functionality:

  • When the user attempts a PR checkout without specifying a PR number, and the user is attached to a TTY (text terminal), this prompt will automatically activate. The prompt will display a list of 10 most recent PRs (tentative) from the base repository
    Users can then select the desired PR from the list using the arrow keys and confirm selection with Enter.
  • Two new unit tests are included

Iteration history

TTY

Screen.Recording.2024-12-09.at.9.25.09.pm.mov

Non TTY
Screenshot 2024-12-06 at 7 44 31 pm

I'd like to thank @BagToad for their constructive feedback throughout the development of this feature.

Second iteration ![2024-11-18 00 03 17](https://github.com/user-attachments/assets/c784fb05-fafd-4b3e-87f9-0948bdb61092)
First iteration Screenshot 2024-11-03 at 11 10 58 AM

@nilvng nilvng marked this pull request as ready for review November 3, 2024 00:43
@nilvng nilvng requested a review from a team as a code owner November 3, 2024 00:43
@nilvng nilvng requested a review from williammartin November 3, 2024 00:43
@cliAutomation cliAutomation added the external pull request originating outside of the CLI core team label Nov 3, 2024
@nilvng nilvng marked this pull request as draft November 17, 2024 04:11
Improve the interactive PR selection UI by
- prefix the PR number with hashcode #
- perserve the text formatting (bold) upon an option is hovered
- add the PR head label

Technical changes:
- Replace \033[0m with \033[39m for maintaining text formatting
@nilvng nilvng marked this pull request as ready for review November 17, 2024 13:08
Copy link
Member

@BagToad BagToad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 Hey @nilvng

Thank you for taking on this old feature request. I am personally looking forward to this feature 😁 ❤️

I added a few comments on the code for things that I think we need to address. I think the team and I may have more comments later, but this should get us moving 😃

I've also noted a few things generally below...


I think we need to address the inverse of the acceptance criteria; prompting should only happen when we are attached to a TTY.

Here is the current behavior when run non-interactively:

image

We must not prompt in this case. Here is an example from gh workflow run showing the desired behavior:

image

Here is the implementation that we can pull inspiration from:

} else if !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("workflow ID, name, or filename required when not running interactively")


There are also some unit test failures that need to be investigated, though I suspect it may be due to some conflict with the trunk branch changes I merged back into your branch 🤔

nilvng

This comment was marked as resolved.

@BagToad
Copy link
Member

BagToad commented Dec 3, 2024

@nilvng Thanks for your continued work on this and insights into your thought process ✨

I've been reviewing this a bit deeper and have a few more things I'd like to address, and I also have a proposal for a refactor.

Interactive checkouts can behave unexpectedly for fork PRs

TLDR: we must include both headRepository and maintainerCanModify in the requested fields even though we don't display those fields to the user when they select the PR. We need to request at least the same fields as the non-interactive flow to later do the necessary Git operations

While testing this, I've noticed that some PRs cause my terminal prompt to behave unexpectedly, and clue me into that the interactive flow was doing different git operations than the non-interactive flow:

image

This is what I'd typically expect instead:

image

I discovered this was because of this logic which occurs later in the process:

if pr.MaintainerCanModify && pr.HeadRepository != nil {
headRepo := ghrepo.NewWithHost(pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name, repoHost)
remote = ghrepo.FormatRemoteURL(headRepo, protocol)
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
}

Since the interactive flow was not requesting these fields ( headRepository and maintainerCanModify), they were false or nil. This causes the the .merge config to be set to a reference to a pull request's head commit instead of a reference to a branch.

Interactive flow

listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{
"number",
"title",
"state",
"url",
"headRefName",
"headRepositoryOwner",
"isCrossRepository",
"isDraft",
"createdAt",

non-interactive flow

pr, baseRepo, err = opts.Finder.Find(shared.FindOptions{
Selector: opts.SelectorArg,
Fields: []string{
"number",
"headRefName",
"headRepository",
"headRepositoryOwner",
"isCrossRepository",
"maintainerCanModify",
},
})

Refactoring interactive flow

I'd like to propose a refactor to the logic around deciding whether to prompt. I'd like to:

  • Align more closely with other command implementations by moving away from the switch statement for this use-case.
  • Improve readability by moving more logic into selectPR, which I've renamed to ResolvePR given it's new behavior.

Note: this proposal includes solutions to the above problem regarding the headRepository and maintainerCanModify fields and solutions to my other comment about moving the prompting check into RunE.

I'd like to push a commit to this PR with my proposal below, if you agree with the essence of the refactor, then we can refactor further if needed ❤️

Please feel free to git apply this diff below to your local branch and let me know what you think 😁 🙌

Edit: here's an example from gh workflow enable that is similar to the refactor I am going for. In that case ResolveWorkflow handles getting a workflow from the user, and it will prompt the user if it needs to - I propose a similar idea with resolvePR that handles getting a PR from the user, enclosing the decision to prompt or not within that function.

Show diff

diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go
index e78eb026..d850f70c 100644
--- a/pkg/cmd/pr/checkout/checkout.go
+++ b/pkg/cmd/pr/checkout/checkout.go
@@ -69,11 +69,14 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
 
 			if len(args) > 0 {
 				opts.SelectorArg = args[0]
+			} else if !opts.IO.CanPrompt() {
+				return cmdutil.FlagErrorf("pull request number, URL, or branch required when not running interactively")
 			}
 
 			if runF != nil {
 				return runF(opts)
 			}
+
 			return checkoutRun(opts)
 		},
 	}
@@ -87,47 +90,21 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
 }
 
 func checkoutRun(opts *CheckoutOptions) error {
-	var (
-		baseRepo ghrepo.Interface
-		pr       *api.PullRequest
-		err      error
-	)
-
-	switch {
-	case opts.SelectorArg != "":
-		pr, baseRepo, err = opts.Finder.Find(shared.FindOptions{
-			Selector: opts.SelectorArg,
-			Fields: []string{
-				"number",
-				"headRefName",
-				"headRepository",
-				"headRepositoryOwner",
-				"isCrossRepository",
-				"maintainerCanModify",
-			},
-		})
-		if err != nil {
-			return err
-		}
-
-	default:
-		if !opts.IO.CanPrompt() {
-			return cmdutil.FlagErrorf("must provide a pull request number (or URL or branch) when not running interactively")
-		}
-
-		httpClient, err := opts.HttpClient()
-		if err != nil {
-			return err
-		}
+	httpClient, err := opts.HttpClient()
+	if err != nil {
+		return err
+	}
 
-		if baseRepo, err = opts.BaseRepo(); err != nil {
-			return err
-		}
+	baseRepo, err := opts.BaseRepo()
+	if err != nil {
+		return err
+	}
 
-		if pr, err = selectPR(httpClient, baseRepo, opts.Prompter, opts.IO.ColorScheme()); err != nil {
-			return err
-		}
+	pr, err := resolvePR(httpClient, baseRepo, opts.Prompter, opts.SelectorArg, opts.Finder, opts.IO.ColorScheme())
+	if err != nil {
+		return err
 	}
+
 	cfg, err := opts.Config()
 	if err != nil {
 		return err
@@ -317,22 +294,46 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm
 	return nil
 }
 
-func selectPR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, cs *iostreams.ColorScheme) (*api.PullRequest, error) {
+func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, pullRequestFinder shared.PRFinder, cs *iostreams.ColorScheme) (*api.PullRequest, error) {
+	// When non-interactive
+	if pullRequestSelector != "" {
+		pr, _, err := pullRequestFinder.Find(shared.FindOptions{
+			Selector: pullRequestSelector,
+			Fields: []string{
+				"number",
+				"headRefName",
+				"headRepository",
+				"headRepositoryOwner",
+				"isCrossRepository",
+				"maintainerCanModify",
+			},
+		})
+		if err != nil {
+			return nil, err
+		}
+		return pr, nil
+	}
+
+	// When interactive
 	listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{
 		"number",
 		"title",
 		"state",
 		"url",
 		"headRefName",
+		"headRepository",
 		"headRepositoryOwner",
 		"isCrossRepository",
 		"isDraft",
 		"createdAt",
+		"maintainerCanModify",
 	}}, 10)
 	if err != nil {
 		return nil, err
 	}
+
 	pr, err := promptForPR(prompter, cs, *listResult)
+
 	return pr, err
 }
 

@nilvng nilvng force-pushed the nil/fix-2329 branch 2 times, most recently from 68ee973 to f1d681e Compare December 3, 2024 09:06
@nilvng
Copy link
Contributor Author

nilvng commented Dec 4, 2024

Hey @BagToad How are you doin!
I'm just wanting to give you a quick update:

  1. On the missing MaintainerCanModify and HeadRepository - very appreciate the catch!
    I was hoping that the unit tests would get my back, as it did before, but seems like there wasn't one in place to catch this particular issue 😓 I expect that the next refactor will enable us to assert the Pull request Fragment similar to Finder() [ref]. Would you have any suggestions on how to implement effective unit tests for this scenario?

  2. Regarding your proposal of ResolvePR() - it makes totally sense to me! The new function maintain the code symmetry which is a huge improvement and def would have prevented the bug! Later after applying the proposed changes the unit tests started failing - I went on to another rabbit holes of unit tests... noticed that opts.BaseRepo was not ever mocked in checkout_test, but AFAIK, it supposed to return the same value. Thus, I made following up changes - keen for your thoughts! 🙌

Show diff
diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go
index 9516e580..b9ded029 100644
--- a/pkg/cmd/pr/checkout/checkout.go
+++ b/pkg/cmd/pr/checkout/checkout.go
@@ -92,47 +92,21 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
 }
 
 func checkoutRun(opts *CheckoutOptions) error {
-	var (
-		baseRepo ghrepo.Interface
-		pr       *api.PullRequest
-		err      error
-	)
-
-	switch {
-	case opts.SelectorArg != "":
-		pr, baseRepo, err = opts.Finder.Find(shared.FindOptions{
-			Selector: opts.SelectorArg,
-			Fields: []string{
-				"number",
-				"headRefName",
-				"headRepository",
-				"headRepositoryOwner",
-				"isCrossRepository",
-				"maintainerCanModify",
-			},
-		})
-		if err != nil {
-			return err
-		}
-
-	default:
-		if !opts.Interactive {
-			return cmdutil.FlagErrorf("must provide a pull request number (or URL or branch) when not running interactively")
-		}
-
-		httpClient, err := opts.HttpClient()
-		if err != nil {
-			return err
-		}
+	baseRepo, err := opts.BaseRepo()
+	if err != nil {
+		return err
+	}
 
-		if baseRepo, err = opts.BaseRepo(); err != nil {
-			return err
-		}
+	client, err := opts.HttpClient()
+	if err != nil {
+		return err
+	}
 
-		if pr, err = selectPR(httpClient, baseRepo, opts.Prompter, opts.IO.ColorScheme()); err != nil {
-			return err
-		}
+	pr, err := resolvePR(client, baseRepo, opts.Prompter, opts.SelectorArg, opts.Interactive, opts.Finder, opts.IO.ColorScheme())
+	if err != nil {
+		return err
 	}
+
 	cfg, err := opts.Config()
 	if err != nil {
 		return err
@@ -322,22 +296,49 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm
 	return nil
 }
 
-func selectPR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, cs *iostreams.ColorScheme) (*api.PullRequest, error) {
+func resolvePR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, pullRequestSelector string, isInteractive bool, pullRequestFinder shared.PRFinder, cs *iostreams.ColorScheme) (*api.PullRequest, error) {
+	// When non-interactive
+	if pullRequestSelector != "" {
+		pr, _, err := pullRequestFinder.Find(shared.FindOptions{
+			Selector: pullRequestSelector,
+			Fields: []string{
+				"number",
+				"headRefName",
+				"headRepository",
+				"headRepositoryOwner",
+				"isCrossRepository",
+				"maintainerCanModify",
+			},
+		})
+		if err != nil {
+			return nil, err
+		}
+		return pr, nil
+	}
+	if !isInteractive {
+		return nil, cmdutil.FlagErrorf("pull request number, URL, or branch required when not running interactively")
+	}
+	// When interactive
 	listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{
 		"number",
 		"title",
 		"state",
 		"url",
+		"isDraft",
+		"createdAt",
+
 		"headRefName",
+		"headRepository",
 		"headRepositoryOwner",
 		"isCrossRepository",
-		"isDraft",
-		"createdAt",
+		"maintainerCanModify",
 	}}, 10)
 	if err != nil {
 		return nil, err
 	}
+
 	pr, err := promptForPR(prompter, cs, *listResult)
+
 	return pr, err
 }
 
diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go
index 49ac0bb2..5c2109f2 100644
--- a/pkg/cmd/pr/checkout/checkout_test.go
+++ b/pkg/cmd/pr/checkout/checkout_test.go
@@ -85,6 +85,10 @@ func Test_checkoutRun(t *testing.T) {
 					finder := shared.NewMockFinder("123", pr, baseRepo)
 					return finder
 				}(),
+				BaseRepo: func() (ghrepo.Interface, error) {
+					baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature")
+					return baseRepo, nil
+				},
 				Config: func() (gh.Config, error) {
 					return config.NewBlankConfig(), nil
 				},
@@ -113,6 +117,10 @@ func Test_checkoutRun(t *testing.T) {
 					finder := shared.NewMockFinder("123", pr, baseRepo)
 					return finder
 				}(),
+				BaseRepo: func() (ghrepo.Interface, error) {
+					baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature")
+					return baseRepo, nil
+				},
 				Config: func() (gh.Config, error) {
 					return config.NewBlankConfig(), nil
 				},
@@ -143,6 +151,10 @@ func Test_checkoutRun(t *testing.T) {
 					finder := shared.NewMockFinder("123", pr, baseRepo)
 					return finder
 				}(),
+				BaseRepo: func() (ghrepo.Interface, error) {
+					baseRepo, _ := stubPR("OWNER/REPO:master", "OWNER/REPO:feature")
+					return baseRepo, nil
+				},
 				Config: func() (gh.Config, error) {
 					return config.NewBlankConfig(), nil
 				},
@@ -171,6 +183,10 @@ func Test_checkoutRun(t *testing.T) {
 					finder := shared.NewMockFinder("123", pr, baseRepo)
 					return finder
 				}(),
+				BaseRepo: func() (ghrepo.Interface, error) {
+					baseRepo, _ := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
+					return baseRepo, nil
+				},
 				Config: func() (gh.Config, error) {
 					return config.NewBlankConfig(), nil
 				},
@@ -196,6 +212,9 @@ func Test_checkoutRun(t *testing.T) {
 			opts: &CheckoutOptions{
 				SelectorArg: "",
 				Interactive: false,
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
 			},
 			remotes: map[string]string{
 				"origin": "OWNER/REPO",
@@ -303,7 +322,7 @@ func Test_checkoutRun(t *testing.T) {
 
 /** LEGACY TESTS **/
 
-func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string) (*test.CmdOut, error) {
+func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string, baseRepo ghrepo.Interface) (*test.CmdOut, error) {
 	ios, _, stdout, stderr := iostreams.Test()
 
 	factory := &cmdutil.Factory{
@@ -332,6 +351,9 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cl
 			GhPath:  "some/path/gh",
 			GitPath: "some/path/git",
 		},
+		BaseRepo: func() (ghrepo.Interface, error) {
+			return baseRepo, nil
+		},
 	}
 
 	cmd := NewCmdCheckout(factory, nil)
@@ -369,7 +391,7 @@ func TestPRCheckout_sameRepo(t *testing.T) {
 	cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
 	cs.Register(`git checkout -b feature --track origin/feature`, 0, "")
 
-	output, err := runCommand(http, nil, "master", `123`)
+	output, err := runCommand(http, nil, "master", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -391,7 +413,7 @@ func TestPRCheckout_existingBranch(t *testing.T) {
 	cs.Register(`git checkout feature`, 0, "")
 	cs.Register(`git merge --ff-only refs/remotes/origin/feature`, 0, "")
 
-	output, err := runCommand(http, nil, "master", `123`)
+	output, err := runCommand(http, nil, "master", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -424,7 +446,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
 	cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
 	cs.Register(`git checkout -b feature --track robot-fork/feature`, 0, "")
 
-	output, err := runCommand(http, remotes, "master", `123`)
+	output, err := runCommand(http, remotes, "master", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -449,7 +471,7 @@ func TestPRCheckout_differentRepo(t *testing.T) {
 	cs.Register(`git config branch\.feature\.pushRemote origin`, 0, "")
 	cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "")
 
-	output, err := runCommand(http, nil, "master", `123`)
+	output, err := runCommand(http, nil, "master", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -470,7 +492,7 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
 	cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
 	cs.Register(`git checkout feature`, 0, "")
 
-	output, err := runCommand(http, nil, "master", `123`)
+	output, err := runCommand(http, nil, "master", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -491,7 +513,7 @@ func TestPRCheckout_detachedHead(t *testing.T) {
 	cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
 	cs.Register(`git checkout feature`, 0, "")
 
-	output, err := runCommand(http, nil, "", `123`)
+	output, err := runCommand(http, nil, "", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -512,7 +534,7 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
 	cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n")
 	cs.Register(`git merge --ff-only FETCH_HEAD`, 0, "")
 
-	output, err := runCommand(http, nil, "feature", `123`)
+	output, err := runCommand(http, nil, "feature", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -528,7 +550,7 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
 	_, cmdTeardown := run.Stub()
 	defer cmdTeardown(t)
 
-	output, err := runCommand(http, nil, "master", `123`)
+	output, err := runCommand(http, nil, "master", `123`, baseRepo)
 	assert.EqualError(t, err, `invalid branch name: "-foo"`)
 	assert.Equal(t, "", output.Stderr())
 	assert.Equal(t, "", output.Stderr())
@@ -553,7 +575,7 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
 	cs.Register(`git config branch\.feature\.pushRemote https://github\.com/hubot/REPO\.git`, 0, "")
 	cs.Register(`git config branch\.feature\.merge refs/heads/feature`, 0, "")
 
-	output, err := runCommand(http, nil, "master", `123`)
+	output, err := runCommand(http, nil, "master", `123`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -576,7 +598,7 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) {
 	cs.Register(`git submodule sync --recursive`, 0, "")
 	cs.Register(`git submodule update --init --recursive`, 0, "")
 
-	output, err := runCommand(http, nil, "master", `123 --recurse-submodules`)
+	output, err := runCommand(http, nil, "master", `123 --recurse-submodules`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())
@@ -597,7 +619,7 @@ func TestPRCheckout_force(t *testing.T) {
 	cs.Register(`git checkout feature`, 0, "")
 	cs.Register(`git reset --hard refs/remotes/origin/feature`, 0, "")
 
-	output, err := runCommand(http, nil, "master", `123 --force`)
+	output, err := runCommand(http, nil, "master", `123 --force`, baseRepo)
 
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
@@ -618,7 +640,7 @@ func TestPRCheckout_detach(t *testing.T) {
 	cs.Register(`git remote get-url origin`, 0, "https://github.com/hubot/REPO.git")
 	cs.Register(`git fetch origin refs/pull/123/head`, 0, "")
 
-	output, err := runCommand(http, nil, "", `123 --detach`)
+	output, err := runCommand(http, nil, "", `123 --detach`, baseRepo)
 	assert.NoError(t, err)
 	assert.Equal(t, "", output.String())
 	assert.Equal(t, "", output.Stderr())

@nilvng
Copy link
Contributor Author

nilvng commented Dec 9, 2024

hey @BagToad cc @williammartin, The core functionality of the PR is now stable 🚀
(huge shoutout to @BagToad for taking time soundboard it with me) 🙇
let me know if you have any questions or concerns, e.g. I'm particularly interested in your thoughts on the baseRepo variable in the legacy unit tests.
I'm going to think through how we might refactor the ListPullRequest() and unit tests per the scope we have defined here

@BagToad
Copy link
Member

BagToad commented Dec 10, 2024

👋 Hey @nilvng - thanks for your continued work and collaboration on this PR ✨ Thank you for updating the color as we discussed and working on the unit tests.

Also wanted to mention that adding the state to the prompt seems like a good idea to me 😁

I'm going to try and address a few things that should hopefully unblock some further development of this feature pending a larger team review, but let me know if I missed anything that you need from us ❤️


I'm particularly interested in your thoughts on the baseRepo variable in the legacy unit tests.

I'm not sure about this one 🤔 I think I'd like to hear the rest of the team's thoughts on this and legacy checkout tests once they have some time.

I'm going to think through how we might refactor the ListPullRequest() and unit tests per the scope we have defined here

Sounds good! I thought a lot about this, and I'm not sure if I know the best solution - we might need to iterate on it. I think we may want to start with following how Finder is implemented and iterate from there; create a new PRLister interface, implementation, and mock, to inject into our Opts.

If I could soundboard my thoughts on what we are testing, I think our testing goals are...

  • Test that checkoutRun is properly using a to-be-created PRLister interface correctly by including MaintainerCanModify and HeadRepository in the query 🤔
  • Our mock PRLister should have a way to check that these values were requested, then we should assert that in the tests.

It leaves a bit to be desired, but let's start there and perhaps the rest of the team will have more feedback as we go. I hope this unblocks you to work on that refactor when desired 😁


let me know if you have any questions or concerns

I do have some things I've been thinking about... 🤔

💭 We should consider a "zero state" message

Current

❯ gh pr checkout
could not prompt: please provide options to select from

Desired (example from run delete)

❯ gh run delete
found no runs to delete

Note

More inconsistency here as the "zero state" for run view is the same 😭 🙃

❯ ghrun view
could not prompt: please provide options to select from

Nonetheless, I think if we're doing this right, I think we should have a message for when there are no PRs found.

💭 Listing only 10 PRs feels like it might not be enough

Note

I don't think any of this blocks this PR. In fact, I'm not convinced this feedback needs to be addressed in this PR. Just noting my concerns for feedback.

I don't know what the right solution is for this, but I wanted to express how this might not be the optimal prompting experience for some folks (like our favorite repo, the cli/cli repo 🤪 ) that have more than 10 PRs.

Just brainstorming some things here:

Allow a --limit flag to control how many PRs are listed in the prompt

Problems: this isn't a precedent that I could find; I cannot find other commands allowing the number of items prompted to be adjusted via a flag like this.

Something that concerns me is the UX of having a flag that only does stuff for the prompting. I think this means that if that's something the team is okay with, we'd want to be very clear with the flag naming.

Don't list draft PRs in the prompt by default, maybe permit listing drafts with a flag

Problems: Same problems as above regarding flags adjusting the content of only the prompt list.

Personal opinion - generally, the PRs I want to checkout are going to be open with some exceptions that I would be happy to pass a flag for. Maybe this behavior could be seen as cumbersome for some that work with drafts often and would dislike that default behavior.

Edit: It's worth mentioning that the reason I bring this up is because excluding drafts would help me squeeze more value out of those 10 PRs that I get to see

Create an environment variable to adjust the number of prompt items for commands that support it including commands like run view

Problems: Perhaps discoverability of an environment variable leaves some to be desired, but I don't think that's a reason to avoid this option. Worth mentioning is that we do support env vars like GH_MDWIDTH, which are adjacently related in that we have an env for adjusting formatting related concerns.

@williammartin
Copy link
Member

@nilvng, @BagToad is on vacation now. I'm sort of loosely following along, let me know if/when you think this is in a position for me to review.

@nilvng
Copy link
Contributor Author

nilvng commented Dec 17, 2024

@williammartin Thanks for checking in! I'd really appreciate your feedback on the PR. I think it's in a good state for review, though I'm still working through a couple of open questions.

For context, here's what I've been working on recently:

  1. Improved Testability of PR required fields for checkout: Refactored ListPullRequest() into the PRLister interface, similar to the existing Finder approach. similar to the existing Finder approach.
  2. Handle the "zero-state" message - where there are no PRs available - we will reuse the message in shared.ListNoResults() e.g. Sprintf("no open %ss in %s", itemName, repoName)

There are also things still are quite up in the air:

  1. Move the GraphQL requests to the api/ module - as it is one mentioned by @BagToad ref Given now that we already have had the PRLister that is shared within the pr/ module, I'm weighing the benefits of improved organization (moving to api/) against keeping things close to where they're used in pr/. What unit tests, and mocks I would need to move along as well

  2. The hard limit of 10 PRs leaves to be desired - I'm still pondering over @BagToad suggestions for a quick win that addresses most use cases without bloating the primary feature of gh pr checkout I would tend to believe that we can iterate on this topic later, but at the same time, I agree with @BagToad that 10 is not optimal solution - While it's sufficient for a smaller startup like mine, I'm concerned it might defeat the usefulness of the feature in larger repositories. super keen for your perspectives on this matter!

@BagToad
Copy link
Member

BagToad commented Jan 26, 2025

👋 Hey @nilvng, thank you for your patience on this. We're getting caught up after the holidays 😁

  • Improved Testability of PR required fields for checkout: Refactored ListPullRequest() into the PRLister interface, similar to the existing Finder approach. similar to the existing Finder approach.
  • Handle the "zero-state" message - where there are no PRs available - we will reuse the message in shared.ListNoResults() e.g. Sprintf("no open %ss in %s", itemName, repoName)

Great work! ✨

I discussed this PR and your questions with @williammartin:

  1. Move the GraphQL requests to the api/ module - as it is one mentioned by @BagToad ref Given now that we already have had the PRLister that is shared within the pr/ module, I'm weighing the benefits of improved organization (moving to api/) against keeping things close to where they're used in pr/. What unit tests, and mocks I would need to move along as well

Neither of us see much value in moving it to an api package right now. Let's keep it beside the PRLister in the pr package👍

2. The hard limit of 10 PRs leaves to be desired - I'm still pondering over @BagToad suggestions for a quick win that addresses most use cases without bloating the primary feature of gh pr checkout I would tend to believe that we can iterate on this topic later, but at the same time, I agree with @BagToad that 10 is not optimal solution - While it's sufficient for a smaller startup like mine, I'm concerned it might defeat the usefulness of the feature in larger repositories. super keen for your perspectives on this matter!

@williammartin and I both agreed that, while there may be improvement opportunities here, it shouldn't block this PR from being merged. So, we'll "ship to learn" on this one 😁 I'm excited to add this feature to my workflow for a few months to really see if the 10 PR limit is even that much of an issue 💭

With all that in mind, I'll work on giving this another review ❤️

@nilvng nilvng requested a review from BagToad February 1, 2025 02:06
Copy link
Member

@BagToad BagToad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 Hey @nilvng - thank you for all the effort and collaboration in this PR ✨ ❤️ Apologies it took so long! I appreciate you taking the leap into implementing an old issue like this and tackling so many unknowns along the way.

I've done another thorough pass, and I think everything looks good and functions well! 😁 🚀

Thank you again for all the work, patience, ideas, and attention to detail ❤️

@BagToad BagToad merged commit fab84be into cli:trunk Feb 1, 2025
9 checks passed
@nilvng
Copy link
Contributor Author

nilvng commented Feb 1, 2025

Hi @BagToad 👋
It's my pleasure to collaborate with you on this little project! Thank you for reviewing my pull request; your feedback was useful and helped me better understand the codebase 🤩 I'm glad to hear that everything looks good, and I look forward to contributing more to the repository in the future.

@nilvng nilvng deleted the nil/fix-2329 branch February 1, 2025 23:14
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Feb 13, 2025
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [cli/cli](https://github.com/cli/cli) | minor | `v2.66.1` -> `v2.67.0` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>cli/cli (cli/cli)</summary>

### [`v2.67.0`](https://github.com/cli/cli/releases/tag/v2.67.0): GitHub CLI 2.67.0

[Compare Source](cli/cli@v2.66.1...v2.67.0)

#### `gh pr checkout` now supports interactively selecting a pull request

Similar to commands like `gh workflow run` which prompts for a workflow to run, now `gh pr checkout` will prompt for a pull request to checkout. The list is currently limited to the most recent 10 pull requests in the repository.

https://github.com/user-attachments/assets/0b2e3761-7318-4573-8a23-ae6f1a44b018

Big thank you to [@&#8203;nilvng](https://github.com/nilvng) for implementing this 🙌

#### Contributing guidelines updated

We've updated our [`CONTRIBUTING.md`](https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md) guidelines to give more clarity around old `help wanted` issues.

*TLDR*:

-   Please directly mention `@cli/code-reviewers` when an issue you want to work on does not have clear Acceptance Criteria
-   Please only open pull requests for issues with *both*  the help wanted label and clear Acceptance Criteria
-   Please avoid expanding pull request scope to include changes that are not described in the connected issue's Acceptance Criteria

Note: Acceptance Criteria is posted as an issue comment by a core maintainer.

See cli/cli#10381 and cli/cli#10395 for more information.

❓ Have feedback on anything? We'd love to hear from you in a discussion post ❤️

#### What's Changed

##### ✨ Features

-   feat: let user select pr to checkout by [@&#8203;nilvng](https://github.com/nilvng) in cli/cli#9868
-   feat: Add support for deleting autolink references by [@&#8203;hoffm](https://github.com/hoffm) in cli/cli#10362
-   \[gh extensions install] Improve help text and error message by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10333
-   Error when `gh repo rename` is used with a new repo name that contains an owner by [@&#8203;timrogers](https://github.com/timrogers) in cli/cli#10364
-   Attestation bundle fetch improvements by [@&#8203;malancas](https://github.com/malancas) in cli/cli#10233
-   \[gh project item-list] Add `iterationId` field in ProjectV2ItemFieldIterationValue by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10329

##### 🐛 Fixes

-   \[gh api] Fix mutual exclusion messages of `--slurp` flag by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10332
-   Exit with error if no matching predicate type exists by [@&#8203;kommendorkapten](https://github.com/kommendorkapten) in cli/cli#10421
-   Do not try to parse bodies for HEAD requests by [@&#8203;jsoref](https://github.com/jsoref) in cli/cli#10388
-   \[gh project item-edit] Fix number type by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10374
-   \[gh workflow run] Improve error handling for `--ref` flag by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10328
-   \[gh config] Escape pipe symbol in Long desc for website manual by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10371

##### 📚 Docs & Chores

-   Fix logic error in contributing docs by [@&#8203;BagToad](https://github.com/BagToad) in cli/cli#10395
-   Docs: Clarify guidelines for `help wanted` issues and pull requests by [@&#8203;BagToad](https://github.com/BagToad) in cli/cli#10381
-   \[gh pr status] Mention `gh pr checks` in the `Long` section by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10389
-   \[docs/releasing.md] Add basic info for homebrew update flow by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10344
-   \[gh issue/pr list] Improve help text by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10335
-   Remove v1 project 'add to board' automation from prauto workflow by [@&#8203;hoffm](https://github.com/hoffm) in cli/cli#10331
-   Note: the following pair of MRs was reverted and never made into a release
    -   \[gh repo edit] Allow setting commit message defaults by [@&#8203;iamazeem](https://github.com/iamazeem) in cli/cli#10363
    -   Revert "\[gh repo edit] Allow setting commit message defaults" by [@&#8203;BagToad](https://github.com/BagToad) in cli/cli#10372

##### :dependabot: Dependencies

-   Bump google.golang.org/protobuf from 1.36.4 to 1.36.5 by [@&#8203;dependabot](https://github.com/dependabot) in cli/cli#10379

**Full Changelog**: cli/cli@v2.66.1...v2.67.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNjUuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiXX0=-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external pull request originating outside of the CLI core team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

let user select pr to checkout (or more) when listing them with pr list

5 participants