-
Notifications
You must be signed in to change notification settings - Fork 7.4k
Rework ref usage when finding and creating PRs #10621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework ref usage when finding and creating PRs #10621
Conversation
f8e918b to
67d2c76
Compare
jtmcg
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely like the direction this is headed
andyfeller
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All in all, I love the changes! I haven't dug into how these affect gh pr view yet but I don't see any major blocking concerns with proceed ✨
67d2c76 to
055f2e0
Compare
c1dfebb to
95ecc45
Compare
| @@ -1,4 +1,4 @@ | |||
| skip 'it creates a fork owned by the user running the test' | |||
| #skip 'it creates a fork owned by the user running the test' | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will revert all these skips these when I'm done.
95ecc45 to
7ce312f
Compare
andyfeller
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just doing a partial review of pkg/cmd/pr packages focused on create, finder, find_refs_resolution by request
tldr: I think these new types / constructs make the code easier to understand. Leaving a variety of questions / nits but nothing to detract or pump the brakes
| QualifiedHeadRef() string | ||
| UnqualifiedHeadRef() string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: how would you explain the difference between qualified and unqualified and which would an contributor or maintainer should use for a given purpose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question, it is a challenging distinction and different between the find and the create. The finder uses the Unqualified form to work with the API, and then uses the Qualified form to match on the returned Pull Requests. The create uses the Qualified form with the API. Unqualified is often used with git, Qualified is often used with UI messages.
andyfeller
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Truly a major effort! There are no major blockers to merging this and finishing the the original PR aside from a few nits.
| # Assert that the PR was created with the correct head repository and refs | ||
| exec gh pr status | ||
| ! stdout 'There is no pull request associated with' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to be clear, the name of this acceptance test is about gh pr status being able to locate the pull request that crosses the user fork-to-upstream relationship, correct?
Beyond the fact that we're using an organization for acceptance testing, would this also work if we tested against a user owned repository rather than an organization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to be clear, the name of this acceptance test is about gh pr status being able to locate the pull request that crosses the user fork-to-upstream relationship, correct?
Yes. Sorry, I should have linked that this PR also fixes #10644
would this also work if we tested against a user owned repository rather than an organization?
The test clones into a user namespace, so I think maybe you've got this inverted? I would not expect it to work for orgs, which we know doesn't work correctly with the ref format :
| cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 1, "") | ||
| cs.Register("git show-ref --verify -- HEAD refs/remotes/origin/feature", 1, "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: are the multiple calls from trying to determine tracking ref? shouldn't this only be called once?
Wondering why we're registering the same git show-ref command twice, I was trying to trace the code to answer the question myself, but not sure why. 🤔
question: is there a reason to mix raw strings and double quoted strings?
Just trying to following the trail:
cli/pkg/cmd/pr/create/create.go
Lines 574 to 579 in ce4f5c5
| // 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) |
cli/pkg/cmd/pr/create/create.go
Lines 688 to 692 in ce4f5c5
| if isPushEnabled { | |
| // TODO: This doesn't respect the @{push} revision resolution or triagular workflows assembled with | |
| // remote.pushDefault, or branch.<branchName>.pushremote config settings. The finder's ParsePRRefs | |
| // may be able to replace this function entirely. | |
| if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found { |
cli/pkg/cmd/pr/create/create.go
Lines 266 to 267 in ce4f5c5
| func createRun(opts *CreateOptions) error { | |
| ctx, err := NewCreateContext(opts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: are the multiple calls from trying to determine tracking ref? shouldn't this only be called once?
I think this is explained in the comments here:
cli/pkg/cmd/pr/create/create.go
Lines 783 to 816 in bee6278
| // If we were able to determine a head repo, then let's check that the remote tracking ref matches the SHA of | |
| // HEAD. If it does, then we don't need to push, otherwise we'll need to ask the user to tell us where to push. | |
| if headRepo, present := defaultPRHead.Repo.Value(); present { | |
| // We may not find a remote because the git branch config may have a URL rather than a remote name. | |
| // Ideally, we would return a sentinel error from RemoteForRepo that we could compare to, but the | |
| // refactor that introduced this code was already large enough. | |
| headRemote, _ := resolvedRemotes.RemoteForRepo(headRepo) | |
| if headRemote != nil { | |
| resolvedRefs, _ := opts.GitClient.ShowRefs( | |
| context.Background(), | |
| []string{ | |
| "HEAD", | |
| fmt.Sprintf("refs/remotes/%s/%s", headRemote.Name, defaultPRHead.BranchName), | |
| }, | |
| ) | |
| // Two refs returned means we can compare HEAD to the remote tracking branch. | |
| // If we had a matching ref, then we can skip pushing. | |
| refsMatch := len(resolvedRefs) == 2 && resolvedRefs[0].Hash == resolvedRefs[1].Hash | |
| if refsMatch { | |
| qualifiedHeadRef := shared.NewQualifiedHeadRefWithoutOwner(defaultPRHead.BranchName) | |
| if headRepo.RepoOwner() != baseRepo.RepoOwner() { | |
| qualifiedHeadRef = shared.NewQualifiedHeadRef(headRepo.RepoOwner(), defaultPRHead.BranchName) | |
| } | |
| return newCreateContext(skipPushRefs{ | |
| qualifiedHeadRef: qualifiedHeadRef, | |
| baseRefs: baseRefs, | |
| }), nil | |
| } | |
| } | |
| } | |
| // If we didn't determine that the git indicated repo had the correct ref, we'll take a look at the other |
TL;DR: if we think we found the repo from git, but it's not on the correct ref, we fall back to guessing. There is a bug in the previous code that makes reuse in both cases wrong. It can be fixed, but I didn't want to expand the scope of changes just to rarely avoid calling show refs twice.
question: is there a reason to mix raw strings and double quoted strings?
Copy and pasting from places that were using heredoc. I'll make them consistent.
| // When the command is run | ||
| reg := &httpmock.Registry{} | ||
| reg.StubRepoInfoResponse("OWNER", "REPO", "master") | ||
| defer reg.Verify(t) | ||
|
|
||
| reg.Register( | ||
| httpmock.GraphQL(`mutation PullRequestCreate\b`), | ||
| httpmock.GraphQLMutation(` | ||
| { "data": { "createPullRequest": { "pullRequest": { | ||
| "URL": "https://github.com/OWNER/REPO/pull/12" | ||
| } } } }`, func(input map[string]interface{}) { | ||
| assert.Equal(t, "REPOID", input["repositoryId"].(string)) | ||
| assert.Equal(t, "master", input["baseRefName"].(string)) | ||
| assert.Equal(t, "OTHEROWNER:feature", input["headRefName"].(string)) | ||
| })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: while following this test, I just wanted to permalink to StubRepoInfoResponse source which is responsible for sourcing these assertion values in order to make sure I was following along
Lines 9 to 23 in ce4f5c5
| func (r *Registry) StubRepoInfoResponse(owner, repo, branch string) { | |
| r.Register( | |
| GraphQL(`query RepositoryInfo\b`), | |
| StringResponse(fmt.Sprintf(` | |
| { "data": { "repository": { | |
| "id": "REPOID", | |
| "name": "%s", | |
| "owner": {"login": "%s"}, | |
| "description": "", | |
| "defaultBranchRef": {"name": "%s"}, | |
| "hasIssuesEnabled": true, | |
| "viewerPermission": "WRITE" | |
| } } } | |
| `, repo, owner, branch))) | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeh it's spooky action at a distance.
| } | ||
| } | ||
|
|
||
| func TestPRFindRefs(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: would love discussing the various architecture approaches to tests like this at some point.
this 1 test contains multiple, parallel test runs including a table-driven test. it makes me step by and wonder the benefits and trade offs to the different approaches, wondering when we choose which.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Strong opinion: Tests that don't share the same assertions shouldn't be tabled together. Weak opinion: Subtests allow for nice behavioural descriptions compared to top level tests. Not much more than that.
jtmcg
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woo! Good work on this. I appreciate where this went and all the heavy lifting you've done. I think only one of my comments is really actionable, but otherwise I'm happy with this. Don't let me be a blocker to others giving this a ✅
Co-authored-by: Andy Feller <[email protected]>
Co-authored-by: Andy Feller <[email protected]>
Co-authored-by: Andy Feller <[email protected]>
8b67d4e
into
kw/575-detect-push-target-for-local-branches-without-upstream-configuration
Description
Relates to #575
Will also fix: #10644
In reviewing #10513 I found it extremely hard to have confidence. Fundamentally, carrying various forms of potentially illegal state representations around, doing conditional checks everywhere, and splitting and concatenating strings is very challenging.
I decided I really, really wanted to encode the valid states where possible in the type system. So for example, instead of having a boolean "isPushEnabled" toggled on and off throughout various conditional behaviour, we collect it all into a
skipPushRefstype that has only the information required.I've made liberal use of
o.Optionto indicate when something is optional. Yes, it's possible to put anilinside ano.Option[ghrepo.Interface]and we don't check for that, which might lead to bugs, but I the approach is still a significant improvement overnilchecks everywhere which convey absolutely nothing about the intended optionality of a piece of data.Finally, I wasted a lot of time trying to converge the refs between create and find, and it didn't work out at all. Fundamentally the acceptable data for each operation varies (
createneeds baseRepo, baseBranch, qualifiedHeadRef, maybe a head repo for push, whilefindneedsbaseRepo,qualifiedHeadRef, optionally abaseBranchand no head repo). Trying to share these data structures resulted in marking lots of data as optional, which just brought back the original problem of the compiler being unable to help.In some cases I have left behaviour as it used to be even if (or because) I didn't fully understand it, or because changing it would expand an already very large PR for less value. I have left comments in these cases.
As of writing, this passes all the Pull Request A/C tests.
Reviewer Notes
It's probably better to read the files top to bottom rather than trying to work the diffs. It might even make more sense to compare to current trunk rather than the PR this is on top of.
I would be surprised even with all this work if we didn't have bugs after shipping. My hope is that bugs will be easier to ferret out and fix in a sustainable manner going forwards, rather than there being no bugs.
Sorry.