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

Skip to content

Commit 0a280ff

Browse files
Merge pull request cli#10110 from cli/10109-document-the-base-repo-resolution-functions
Document the base repo resolution functions
2 parents 5402e20 + b9c5974 commit 0a280ff

File tree

1 file changed

+95
-2
lines changed

1 file changed

+95
-2
lines changed

pkg/cmd/factory/default.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,31 @@ func New(appVersion string) *cmdutil.Factory {
4444
return f
4545
}
4646

47+
// BaseRepoFunc requests a list of Remotes, and selects the first one.
48+
// Although Remotes is injected via the factory so it looks like the function might
49+
// be configurable, in practice, it's calling readRemotes, and the injection is indirection.
50+
//
51+
// readRemotes makes use of the remoteResolver, which is responsible for requesting the list
52+
// of remotes for the current working directory from git. It then does some filtering to
53+
// only retain remotes for hosts that we have authenticated against; keep in mind this may
54+
// be the single value of GH_HOST.
55+
//
56+
// That list of remotes is sorted by their remote name, in the following order:
57+
// 1. upstream
58+
// 2. github
59+
// 3. origin
60+
// 4. other remotes, no ordering guaratanteed because the sort function is not stable
61+
//
62+
// Given that list, this function chooses the first one.
63+
//
64+
// Here's a common example of when this might matter: when we clone a fork, by default we add
65+
// the parent as a remote named upstream. So the remotes may look like this:
66+
// upstream https://github.com/cli/cli.git (fetch)
67+
// upstream https://github.com/cli/cli.git (push)
68+
// origin https://github.com/cli/cli-fork.git (fetch)
69+
// origin https://github.com/cli/cli-fork.git (push)
70+
//
71+
// With this resolution function, the upstream will always be chosen (assuming we have authenticated with github.com).
4772
func BaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
4873
return func() (ghrepo.Interface, error) {
4974
remotes, err := f.Remotes()
@@ -54,6 +79,74 @@ func BaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
5479
}
5580
}
5681

82+
// SmartBaseRepoFunc provides additional behaviour over BaseRepoFunc. Read the BaseRepoFunc
83+
// documentation for more information on how remotes are fetched and ordered.
84+
//
85+
// Unlike BaseRepoFunc, instead of selecting the first remote in the list, this function will
86+
// use the API to resolve repository networks, and attempt to use the `resolved` git remote config value
87+
// as part of determining the base repository.
88+
//
89+
// Although the behaviour commented below really belongs to the `BaseRepo` function on `ResolvedRemotes`,
90+
// in practice the most important place to understand the general behaviour is here, so that's where
91+
// I'm going to write it.
92+
//
93+
// Firstly, the remotes are inspected to see whether any are already resolved. Resolution means the git
94+
// config value of the `resolved` key was `base` (meaning this remote is the base repository), or a specific
95+
// repository e.g. `cli/cli` (meaning that specific repo is the base repo, regardless of whether a remote
96+
// exists for it). These values are set by default on clone of a fork, or by running `repo set-default`. If
97+
// either are set, that repository is returned.
98+
//
99+
// If we the current invocation is unable to prompt, then the first remote is returned. I believe this behaviour
100+
// exists for backwards compatibility before the later steps were introduced, however, this is frequently a source
101+
// of differing behaviour between interactive and non-interactive invocations:
102+
//
103+
// ➜ git remote -v
104+
// origin https://github.com/williammartin/test-repo.git (fetch)
105+
// origin https://github.com/williammartin/test-repo.git (push)
106+
// upstream https://github.com/williammartin-test-org/test-repo.git (fetch)
107+
// upstream https://github.com/williammartin-test-org/test-repo.git (push)
108+
//
109+
// ➜ gh pr list
110+
// X No default remote repository has been set for this directory.
111+
//
112+
// please run `gh repo set-default` to select a default remote repository.
113+
// ➜ gh pr list | cat
114+
// 3 test williammartin-test-org:remote-push-default-feature OPEN 2024-12-13T10:28:40Z
115+
//
116+
// Furthermore, when repositories have been renamed on the server and not on the local git remote, this causes
117+
// even more confusion because the API requests can be different, and FURTHERMORE this can be an issue for
118+
// services that don't handle renames correctly, like the ElasticSearch indexing.
119+
//
120+
// Assuming we have an interactive invocation, then the next step is to resolve a network of respositories. This
121+
// involves creating a dynamic GQL query requesting information about each repository (up to a limit of 5).
122+
// Each returned repo is added to a list, along with its parent, if present in the query response.
123+
// The repositories in the query retain the same ordering as previously outlined. Interestingly, the request is sent
124+
// to the hostname of the first repo, so if you happen to have remotes on different GitHub hosts, then they won't
125+
// resolve correctly. I'm not sure this has ever caused an issue, but does seem like a potential source of bugs.
126+
// In practice, since the remotes are ordered with upstream, github, origin before others, it's almost always going
127+
// to be the case that the correct host is chosen.
128+
//
129+
// Because fetching the network includes the parent repo, even if it is not a remote, this requires the user to
130+
// disambiguate, which can be surprising, though I'm not sure I've heard anyone complain:
131+
//
132+
// ➜ git remote -v
133+
// origin https://github.com/williammartin/test-repo.git (fetch)
134+
// origin https://github.com/williammartin/test-repo.git (push)
135+
//
136+
// ➜ gh pr list
137+
// X No default remote repository has been set for this directory.
138+
//
139+
// please run `gh repo set-default` to select a default remote repository.
140+
//
141+
// If no repos are returned from the API then we return the first remote from the original list. I'm not sure
142+
// why we do this rather than erroring, because it seems like almost every future step is going to fail when hitting
143+
// the API. Potentially it helps if there is an API blip? It was added without comment in:
144+
// https://github.com/cli/cli/pull/1706/files#diff-65730f0373fb91dd749940cf09daeaf884e5643d665a6c3eb09d54785a6d475eR113
145+
//
146+
// If one repo is returned from the API, then that one is returned as the base repo.
147+
//
148+
// If more than one repo is returned from the API, we indicate to the user that they need to run `repo set-default`,
149+
// and return an error with no base repo.
57150
func SmartBaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
58151
return func() (ghrepo.Interface, error) {
59152
httpClient, err := f.HttpClient()
@@ -67,11 +160,11 @@ func SmartBaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
67160
if err != nil {
68161
return nil, err
69162
}
70-
repoContext, err := ghContext.ResolveRemotesToRepos(remotes, apiClient, "")
163+
resolvedRepos, err := ghContext.ResolveRemotesToRepos(remotes, apiClient, "")
71164
if err != nil {
72165
return nil, err
73166
}
74-
baseRepo, err := repoContext.BaseRepo(f.IOStreams)
167+
baseRepo, err := resolvedRepos.BaseRepo(f.IOStreams)
75168
if err != nil {
76169
return nil, err
77170
}

0 commit comments

Comments
 (0)