@@ -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).
4772func 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.
57150func 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