1+ // TODO: rename this package to avoid clash with stdlib
12package context
23
34import (
45 "errors"
5- "fmt"
6- "os"
76 "sort"
8- "strings"
97
8+ "github.com/AlecAivazis/survey/v2"
109 "github.com/cli/cli/api"
11- "github.com/cli/cli/internal/config "
10+ "github.com/cli/cli/git "
1211 "github.com/cli/cli/internal/ghrepo"
12+ "github.com/cli/cli/pkg/iostreams"
13+ "github.com/cli/cli/pkg/prompt"
1314)
1415
15- // Context represents the interface for querying information about the current environment
16- type Context interface {
17- Config () (config.Config , error )
18- }
19-
2016// cap the number of git remotes looked up, since the user might have an
2117// unusually large number of git remotes
2218const maxRemotesForLookup = 5
2319
24- // ResolveRemotesToRepos takes in a list of git remotes and fetches more information about the repositories they map to.
25- // Only the git remotes belonging to the same hostname are ever looked up; all others are ignored.
26- func ResolveRemotesToRepos (remotes Remotes , client * api.Client , base string ) (ResolvedRemotes , error ) {
20+ func ResolveRemotesToRepos (remotes Remotes , client * api.Client , base string ) (* ResolvedRemotes , error ) {
2721 sort .Stable (remotes )
2822
29- result := ResolvedRemotes {
30- Remotes : remotes ,
23+ result := & ResolvedRemotes {
24+ remotes : remotes ,
3125 apiClient : client ,
3226 }
3327
@@ -38,138 +32,136 @@ func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (Re
3832 if err != nil {
3933 return result , err
4034 }
41- result .BaseOverride = baseOverride
35+ result .baseOverride = baseOverride
4236 }
4337
44- foundBaseOverride := false
45- var hostname string
38+ return result , nil
39+ }
40+
41+ func resolveNetwork (result * ResolvedRemotes ) error {
4642 var repos []ghrepo.Interface
47- for i , r := range remotes {
48- if i == 0 {
49- hostname = r .RepoHost ()
50- } else if ! strings .EqualFold (r .RepoHost (), hostname ) {
51- // ignore all remotes for a hostname different to that of the 1st remote
52- continue
53- }
43+ for _ , r := range result .remotes {
5444 repos = append (repos , r )
55- if baseOverride != nil && ghrepo .IsSame (r , baseOverride ) {
56- foundBaseOverride = true
57- }
5845 if len (repos ) == maxRemotesForLookup {
5946 break
6047 }
6148 }
62- if baseOverride != nil && ! foundBaseOverride {
63- // additionally, look up the explicitly specified base repo if it's not
64- // already covered by git remotes
65- repos = append (repos , baseOverride )
66- }
6749
68- networkResult , err := api .RepoNetwork (client , repos )
69- if err != nil {
70- return result , err
71- }
72- result .Network = networkResult
73- return result , nil
50+ networkResult , err := api .RepoNetwork (result .apiClient , repos )
51+ result .network = & networkResult
52+ return err
7453}
7554
7655type ResolvedRemotes struct {
77- BaseOverride ghrepo.Interface
78- Remotes Remotes
79- Network api.RepoNetworkResult
56+ baseOverride ghrepo.Interface
57+ remotes Remotes
58+ network * api.RepoNetworkResult
8059 apiClient * api.Client
8160}
8261
83- // BaseRepo is the first found repository in the "upstream", "github", "origin"
84- // git remote order, resolved to the parent repo if the git remote points to a fork
85- func (r ResolvedRemotes ) BaseRepo () (* api.Repository , error ) {
86- if r .BaseOverride != nil {
87- for _ , repo := range r .Network .Repositories {
88- if repo != nil && ghrepo .IsSame (repo , r .BaseOverride ) {
89- 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
9075 }
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
90+ }
91+ }
92+
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 )
91100 }
92- return nil , fmt .Errorf ("failed looking up information about the '%s' repository" ,
93- ghrepo .FullName (r .BaseOverride ))
94101 }
95102
96- for _ , repo := range r .Network .Repositories {
103+ for _ , repo := range r .network .Repositories {
97104 if repo == nil {
98105 continue
99106 }
100107 if repo .IsFork () {
101- return repo .Parent , nil
108+ add ( repo .Parent )
102109 }
103- return repo , nil
110+ add ( repo )
104111 }
105112
106- return nil , errors .New ("not found" )
107- }
108-
109- // HeadRepo is a fork of base repo (if any), or the first found repository that
110- // has push access
111- func (r ResolvedRemotes ) HeadRepo () (* api.Repository , error ) {
112- baseRepo , err := r .BaseRepo ()
113- if err != nil {
114- return nil , err
113+ if len (repoNames ) == 0 {
114+ return r .remotes [0 ], nil
115115 }
116116
117- // try to find a pushable fork among existing remotes
118- for _ , repo := range r .Network .Repositories {
119- if repo != nil && repo .Parent != nil && repo .ViewerCanPush () && ghrepo .IsSame (repo .Parent , baseRepo ) {
120- return repo , nil
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
121125 }
122126 }
123127
124- // a fork might still exist on GitHub, so let's query for it
125- var notFound * api.NotFoundError
126- if repo , err := api .RepoFindFork (r .apiClient , baseRepo ); err == nil {
127- return repo , nil
128- } else if ! errors .As (err , & notFound ) {
129- return nil , err
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
140+ }
141+
142+ func (r * ResolvedRemotes ) HeadRepos () ([]* api.Repository , error ) {
143+ if r .network == nil {
144+ err := resolveNetwork (r )
145+ if err != nil {
146+ return nil , err
147+ }
130148 }
131149
132- // fall back to any listed repository that has push access
133- for _ , repo := range r .Network .Repositories {
150+ var results [] * api. Repository
151+ for _ , repo := range r .network .Repositories {
134152 if repo != nil && repo .ViewerCanPush () {
135- return repo , nil
153+ results = append ( results , repo )
136154 }
137155 }
138- return nil , errors . New ( "none of the repositories have push access" )
156+ return results , nil
139157}
140158
141159// RemoteForRepo finds the git remote that points to a repository
142- func (r ResolvedRemotes ) RemoteForRepo (repo ghrepo.Interface ) (* Remote , error ) {
143- for i , remote := range r .Remotes {
144- if ghrepo .IsSame (remote , repo ) ||
145- // additionally, look up the resolved repository name in case this
146- // git remote points to this repository via a redirect
147- (r .Network .Repositories [i ] != nil && ghrepo .IsSame (r .Network .Repositories [i ], repo )) {
160+ func (r * ResolvedRemotes ) RemoteForRepo (repo ghrepo.Interface ) (* Remote , error ) {
161+ for _ , remote := range r .remotes {
162+ if ghrepo .IsSame (remote , repo ) {
148163 return remote , nil
149164 }
150165 }
151166 return nil , errors .New ("not found" )
152167}
153-
154- // New initializes a Context that reads from the filesystem
155- func New () Context {
156- return & fsContext {}
157- }
158-
159- // A Context implementation that queries the filesystem
160- type fsContext struct {
161- config config.Config
162- }
163-
164- func (c * fsContext ) Config () (config.Config , error ) {
165- if c .config == nil {
166- cfg , err := config .ParseDefaultConfig ()
167- if errors .Is (err , os .ErrNotExist ) {
168- cfg = config .NewBlankConfig ()
169- } else if err != nil {
170- return nil , err
171- }
172- c .config = cfg
173- }
174- return c .config , nil
175- }
0 commit comments