github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/context/context.go (about) 1 // TODO: rename this package to avoid clash with stdlib 2 package context 3 4 import ( 5 "context" 6 "errors" 7 "sort" 8 9 "github.com/ungtb10d/cli/v2/api" 10 "github.com/ungtb10d/cli/v2/git" 11 "github.com/ungtb10d/cli/v2/internal/ghrepo" 12 "github.com/ungtb10d/cli/v2/pkg/iostreams" 13 ) 14 15 // cap the number of git remotes looked up, since the user might have an 16 // unusually large number of git remotes 17 const maxRemotesForLookup = 5 18 19 func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (*ResolvedRemotes, error) { 20 sort.Stable(remotes) 21 22 result := &ResolvedRemotes{ 23 remotes: remotes, 24 apiClient: client, 25 } 26 27 var baseOverride ghrepo.Interface 28 if base != "" { 29 var err error 30 baseOverride, err = ghrepo.FromFullName(base) 31 if err != nil { 32 return result, err 33 } 34 result.baseOverride = baseOverride 35 } 36 37 return result, nil 38 } 39 40 func resolveNetwork(result *ResolvedRemotes) error { 41 var repos []ghrepo.Interface 42 for _, r := range result.remotes { 43 repos = append(repos, r) 44 if len(repos) == maxRemotesForLookup { 45 break 46 } 47 } 48 49 networkResult, err := api.RepoNetwork(result.apiClient, repos) 50 result.network = &networkResult 51 return err 52 } 53 54 type ResolvedRemotes struct { 55 baseOverride ghrepo.Interface 56 remotes Remotes 57 network *api.RepoNetworkResult 58 apiClient *api.Client 59 } 60 61 type iprompter interface { 62 Select(string, string, []string) (int, error) 63 } 64 65 func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams, p iprompter) (ghrepo.Interface, error) { 66 if r.baseOverride != nil { 67 return r.baseOverride, nil 68 } 69 70 // if any of the remotes already has a resolution, respect that 71 for _, r := range r.remotes { 72 if r.Resolved == "base" { 73 return r, nil 74 } else if r.Resolved != "" { 75 repo, err := ghrepo.FromFullName(r.Resolved) 76 if err != nil { 77 return nil, err 78 } 79 return ghrepo.NewWithHost(repo.RepoOwner(), repo.RepoName(), r.RepoHost()), nil 80 } 81 } 82 83 if !io.CanPrompt() { 84 // we cannot prompt, so just resort to the 1st remote 85 return r.remotes[0], nil 86 } 87 88 // from here on, consult the API 89 if r.network == nil { 90 err := resolveNetwork(r) 91 if err != nil { 92 return nil, err 93 } 94 } 95 96 var repoNames []string 97 repoMap := map[string]*api.Repository{} 98 add := func(r *api.Repository) { 99 fn := ghrepo.FullName(r) 100 if _, ok := repoMap[fn]; !ok { 101 repoMap[fn] = r 102 repoNames = append(repoNames, fn) 103 } 104 } 105 106 for _, repo := range r.network.Repositories { 107 if repo == nil { 108 continue 109 } 110 if repo.Parent != nil { 111 add(repo.Parent) 112 } 113 add(repo) 114 } 115 116 if len(repoNames) == 0 { 117 return r.remotes[0], nil 118 } 119 120 baseName := repoNames[0] 121 if len(repoNames) > 1 { 122 // hide the spinner in case a command started the progress indicator before base repo was fully 123 // resolved, e.g. in `gh issue view` 124 io.StopProgressIndicator() 125 selected, err := p.Select("Which should be the base repository (used for e.g. querying issues) for this directory?", "", repoNames) 126 if err != nil { 127 return nil, err 128 } 129 baseName = repoNames[selected] 130 } 131 132 // determine corresponding git remote 133 selectedRepo := repoMap[baseName] 134 resolution := "base" 135 remote, _ := r.RemoteForRepo(selectedRepo) 136 if remote == nil { 137 remote = r.remotes[0] 138 resolution = ghrepo.FullName(selectedRepo) 139 } 140 141 // cache the result to git config 142 c := &git.Client{} 143 err := c.SetRemoteResolution(context.Background(), remote.Name, resolution) 144 return selectedRepo, err 145 } 146 147 func (r *ResolvedRemotes) HeadRepos() ([]*api.Repository, error) { 148 if r.network == nil { 149 err := resolveNetwork(r) 150 if err != nil { 151 return nil, err 152 } 153 } 154 155 var results []*api.Repository 156 for _, repo := range r.network.Repositories { 157 if repo != nil && repo.ViewerCanPush() { 158 results = append(results, repo) 159 } 160 } 161 return results, nil 162 } 163 164 // RemoteForRepo finds the git remote that points to a repository 165 func (r *ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) { 166 for _, remote := range r.remotes { 167 if ghrepo.IsSame(remote, repo) { 168 return remote, nil 169 } 170 } 171 return nil, errors.New("not found") 172 }