X Tutup
Skip to content

Commit b2e36a0

Browse files
authored
Merge pull request cli#1706 from cli/base-resolve
Improve repository base and head resolution
2 parents bdadb30 + f99a554 commit b2e36a0

File tree

13 files changed

+410
-1105
lines changed

13 files changed

+410
-1105
lines changed

api/queries_repo.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
9191
query RepositoryInfo($owner: String!, $name: String!) {
9292
repository(owner: $owner, name: $name) {
9393
id
94+
name
95+
owner { login }
9496
hasIssuesEnabled
9597
description
9698
viewerPermission
@@ -317,8 +319,8 @@ func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
317319
}, nil
318320
}
319321

320-
// RepoFindFork finds a fork of repo affiliated with the viewer
321-
func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
322+
// RepoFindForks finds forks of the repo that are affiliated with the viewer
323+
func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Repository, error) {
322324
result := struct {
323325
Repository struct {
324326
Forks struct {
@@ -330,12 +332,13 @@ func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
330332
variables := map[string]interface{}{
331333
"owner": repo.RepoOwner(),
332334
"repo": repo.RepoName(),
335+
"limit": limit,
333336
}
334337

335338
if err := client.GraphQL(repo.RepoHost(), `
336-
query RepositoryFindFork($owner: String!, $repo: String!) {
339+
query RepositoryFindFork($owner: String!, $repo: String!, $limit: Int!) {
337340
repository(owner: $owner, name: $repo) {
338-
forks(first: 1, affiliations: [OWNER, COLLABORATOR]) {
341+
forks(first: $limit, affiliations: [OWNER, COLLABORATOR]) {
339342
nodes {
340343
id
341344
name
@@ -350,14 +353,18 @@ func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
350353
return nil, err
351354
}
352355

353-
forks := result.Repository.Forks.Nodes
354-
// we check ViewerCanPush, even though we expect it to always be true per
355-
// `affiliations` condition, to guard against versions of GitHub with a
356-
// faulty `affiliations` implementation
357-
if len(forks) > 0 && forks[0].ViewerCanPush() {
358-
return InitRepoHostname(&forks[0], repo.RepoHost()), nil
356+
var results []*Repository
357+
for _, r := range result.Repository.Forks.Nodes {
358+
// we check ViewerCanPush, even though we expect it to always be true per
359+
// `affiliations` condition, to guard against versions of GitHub with a
360+
// faulty `affiliations` implementation
361+
if !r.ViewerCanPush() {
362+
continue
363+
}
364+
results = append(results, InitRepoHostname(&r, repo.RepoHost()))
359365
}
360-
return nil, &NotFoundError{errors.New("no fork found")}
366+
367+
return results, nil
361368
}
362369

363370
type RepoMetadataResult struct {

context/blank_context.go

Lines changed: 0 additions & 24 deletions
This file was deleted.

context/context.go

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

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

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

Comments
 (0)
X Tutup