1+ // TODO: rename this package to avoid clash with stdlib
12package context
23
34import (
45 "errors"
5- "fmt"
66 "sort"
7- "strings"
87
8+ "github.com/AlecAivazis/survey/v2"
99 "github.com/cli/cli/api"
10+ "github.com/cli/cli/git"
1011 "github.com/cli/cli/internal/ghrepo"
12+ "github.com/cli/cli/pkg/iostreams"
13+ "github.com/cli/cli/pkg/prompt"
1114)
1215
1316// cap the number of git remotes looked up, since the user might have an
1417// unusually large number of git remotes
1518const maxRemotesForLookup = 5
1619
17- // ResolveRemotesToRepos takes in a list of git remotes and fetches more information about the repositories they map to.
18- // Only the git remotes belonging to the same hostname are ever looked up; all others are ignored.
19- func ResolveRemotesToRepos (remotes Remotes , client * api.Client , base string ) (ResolvedRemotes , error ) {
20+ func ResolveRemotesToRepos (remotes Remotes , client * api.Client , base string ) (* ResolvedRemotes , error ) {
2021 sort .Stable (remotes )
2122
22- result := ResolvedRemotes {
23- Remotes : remotes ,
23+ result := & ResolvedRemotes {
24+ remotes : remotes ,
2425 apiClient : client ,
2526 }
2627
@@ -31,84 +32,123 @@ func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (Re
3132 if err != nil {
3233 return result , err
3334 }
34- result .BaseOverride = baseOverride
35+ result .baseOverride = baseOverride
3536 }
3637
37- foundBaseOverride := false
38- var hostname string
38+ return result , nil
39+ }
40+
41+ func resolveNetwork (result * ResolvedRemotes ) error {
3942 var repos []ghrepo.Interface
40- for i , r := range remotes {
41- if i == 0 {
42- hostname = r .RepoHost ()
43- } else if ! strings .EqualFold (r .RepoHost (), hostname ) {
44- // ignore all remotes for a hostname different to that of the 1st remote
45- continue
46- }
43+ for _ , r := range result .remotes {
4744 repos = append (repos , r )
48- if baseOverride != nil && ghrepo .IsSame (r , baseOverride ) {
49- foundBaseOverride = true
50- }
5145 if len (repos ) == maxRemotesForLookup {
5246 break
5347 }
5448 }
55- if baseOverride != nil && ! foundBaseOverride {
56- // additionally, look up the explicitly specified base repo if it's not
57- // already covered by git remotes
58- repos = append (repos , baseOverride )
59- }
6049
61- networkResult , err := api .RepoNetwork (client , repos )
62- if err != nil {
63- return result , err
64- }
65- result .Network = networkResult
66- return result , nil
50+ networkResult , err := api .RepoNetwork (result .apiClient , repos )
51+ result .network = & networkResult
52+ return err
6753}
6854
6955type ResolvedRemotes struct {
70- BaseOverride ghrepo.Interface
71- Remotes Remotes
72- Network api.RepoNetworkResult
56+ baseOverride ghrepo.Interface
57+ remotes Remotes
58+ network * api.RepoNetworkResult
7359 apiClient * api.Client
7460}
7561
76- // BaseRepo is the first found repository in the "upstream", "github", "origin"
77- // git remote order, resolved to the parent repo if the git remote points to a fork
78- func (r ResolvedRemotes ) BaseRepo () (* api.Repository , error ) {
79- if r .BaseOverride != nil {
80- for _ , repo := range r .Network .Repositories {
81- if repo != nil && ghrepo .IsSame (repo , r .BaseOverride ) {
82- return repo , nil
62+ func (r * ResolvedRemotes ) BaseRepo (io * iostreams.IOStreams ) (ghrepo.Interface , error ) {
63+ if r .baseOverride != nil {
64+ return r .baseOverride , nil
65+ }
66+
67+ // if any of the remotes already has a resolution, respect that
68+ for _ , r := range r .remotes {
69+ if r .Resolved == "base" {
70+ return r , nil
71+ } else if r .Resolved != "" {
72+ repo , err := ghrepo .FromFullName (r .Resolved )
73+ if err != nil {
74+ return nil , err
8375 }
76+ return ghrepo .NewWithHost (repo .RepoOwner (), repo .RepoName (), r .RepoHost ()), nil
77+ }
78+ }
79+
80+ if ! io .CanPrompt () {
81+ // we cannot prompt, so just resort to the 1st remote
82+ return r .remotes [0 ], nil
83+ }
84+
85+ // from here on, consult the API
86+ if r .network == nil {
87+ err := resolveNetwork (r )
88+ if err != nil {
89+ return nil , err
8490 }
85- return nil , fmt .Errorf ("failed looking up information about the '%s' repository" ,
86- ghrepo .FullName (r .BaseOverride ))
8791 }
8892
89- for _ , repo := range r .Network .Repositories {
93+ var repoNames []string
94+ repoMap := map [string ]* api.Repository {}
95+ add := func (r * api.Repository ) {
96+ fn := ghrepo .FullName (r )
97+ if _ , ok := repoMap [fn ]; ! ok {
98+ repoMap [fn ] = r
99+ repoNames = append (repoNames , fn )
100+ }
101+ }
102+
103+ for _ , repo := range r .network .Repositories {
90104 if repo == nil {
91105 continue
92106 }
93107 if repo .IsFork () {
94- return repo .Parent , nil
108+ add ( repo .Parent )
95109 }
96- return repo , nil
110+ add ( repo )
97111 }
98112
99- return nil , errors .New ("not found" )
113+ if len (repoNames ) == 0 {
114+ return r .remotes [0 ], nil
115+ }
116+
117+ baseName := repoNames [0 ]
118+ if len (repoNames ) > 1 {
119+ err := prompt .SurveyAskOne (& survey.Select {
120+ Message : "Which should be the base repository (used for e.g. querying issues) for this directory?" ,
121+ Options : repoNames ,
122+ }, & baseName )
123+ if err != nil {
124+ return nil , err
125+ }
126+ }
127+
128+ // determine corresponding git remote
129+ selectedRepo := repoMap [baseName ]
130+ resolution := "base"
131+ remote , _ := r .RemoteForRepo (selectedRepo )
132+ if remote == nil {
133+ remote = r .remotes [0 ]
134+ resolution = ghrepo .FullName (selectedRepo )
135+ }
136+
137+ // cache the result to git config
138+ err := git .SetRemoteResolution (remote .Name , resolution )
139+ return selectedRepo , err
100140}
101141
102- // HeadRepo is a fork of base repo (if any), or the first found repository that
103- // has push access
104- func ( r ResolvedRemotes ) HeadRepo () ( * api. Repository , error ) {
105- baseRepo , err := r . BaseRepo ()
106- if err != nil {
107- return nil , err
142+ func ( r * ResolvedRemotes ) HeadRepo ( baseRepo ghrepo. Interface ) (ghrepo. Interface , error ) {
143+ if r . network == nil {
144+ err := resolveNetwork ( r )
145+ if err != nil {
146+ return nil , err
147+ }
108148 }
109149
110150 // try to find a pushable fork among existing remotes
111- for _ , repo := range r .Network .Repositories {
151+ for _ , repo := range r .network .Repositories {
112152 if repo != nil && repo .Parent != nil && repo .ViewerCanPush () && ghrepo .IsSame (repo .Parent , baseRepo ) {
113153 return repo , nil
114154 }
@@ -123,7 +163,7 @@ func (r ResolvedRemotes) HeadRepo() (*api.Repository, error) {
123163 }
124164
125165 // fall back to any listed repository that has push access
126- for _ , repo := range r .Network .Repositories {
166+ for _ , repo := range r .network .Repositories {
127167 if repo != nil && repo .ViewerCanPush () {
128168 return repo , nil
129169 }
@@ -132,12 +172,9 @@ func (r ResolvedRemotes) HeadRepo() (*api.Repository, error) {
132172}
133173
134174// RemoteForRepo finds the git remote that points to a repository
135- func (r ResolvedRemotes ) RemoteForRepo (repo ghrepo.Interface ) (* Remote , error ) {
136- for i , remote := range r .Remotes {
137- if ghrepo .IsSame (remote , repo ) ||
138- // additionally, look up the resolved repository name in case this
139- // git remote points to this repository via a redirect
140- (r .Network .Repositories [i ] != nil && ghrepo .IsSame (r .Network .Repositories [i ], repo )) {
175+ func (r * ResolvedRemotes ) RemoteForRepo (repo ghrepo.Interface ) (* Remote , error ) {
176+ for _ , remote := range r .remotes {
177+ if ghrepo .IsSame (remote , repo ) {
141178 return remote , nil
142179 }
143180 }
0 commit comments