Skip to content

Commit d534a94

Browse files
committed
Change how base repository is resolved
On first run in a git repository, `BaseRepo()` will now prompt the user which repository should be queried as base repository if there are multiple git remotes or when we are in the context of a fork. In non-interactive mode, the prompt is skipped and we default to the first git remote instead. After the base repo is resolved, the result is cached in the local repository using `git config` so that RepositoryNetwork API lookups can be avoided in the future.
1 parent 969321b commit d534a94

File tree

5 files changed

+143
-229
lines changed

5 files changed

+143
-229
lines changed

context/context.go

Lines changed: 98 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
1+
// TODO: rename this package to avoid clash with stdlib
12
package context
23

34
import (
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
1518
const 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

6955
type 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

Comments
 (0)