Skip to content

Commit

Permalink
Merge pull request cli#1706 from cli/base-resolve
Browse files Browse the repository at this point in the history
Improve repository base and head resolution
  • Loading branch information
mislav authored Sep 16, 2020
2 parents bdadb30 + f99a554 commit b2e36a0
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 1,105 deletions.
29 changes: 18 additions & 11 deletions api/queries_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
query RepositoryInfo($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
name
owner { login }
hasIssuesEnabled
description
viewerPermission
Expand Down Expand Up @@ -317,8 +319,8 @@ func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
}, nil
}

// RepoFindFork finds a fork of repo affiliated with the viewer
func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
// RepoFindForks finds forks of the repo that are affiliated with the viewer
func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Repository, error) {
result := struct {
Repository struct {
Forks struct {
Expand All @@ -330,12 +332,13 @@ func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"limit": limit,
}

if err := client.GraphQL(repo.RepoHost(), `
query RepositoryFindFork($owner: String!, $repo: String!) {
query RepositoryFindFork($owner: String!, $repo: String!, $limit: Int!) {
repository(owner: $owner, name: $repo) {
forks(first: 1, affiliations: [OWNER, COLLABORATOR]) {
forks(first: $limit, affiliations: [OWNER, COLLABORATOR]) {
nodes {
id
name
Expand All @@ -350,14 +353,18 @@ func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
return nil, err
}

forks := result.Repository.Forks.Nodes
// we check ViewerCanPush, even though we expect it to always be true per
// `affiliations` condition, to guard against versions of GitHub with a
// faulty `affiliations` implementation
if len(forks) > 0 && forks[0].ViewerCanPush() {
return InitRepoHostname(&forks[0], repo.RepoHost()), nil
var results []*Repository
for _, r := range result.Repository.Forks.Nodes {
// we check ViewerCanPush, even though we expect it to always be true per
// `affiliations` condition, to guard against versions of GitHub with a
// faulty `affiliations` implementation
if !r.ViewerCanPush() {
continue
}
results = append(results, InitRepoHostname(&r, repo.RepoHost()))
}
return nil, &NotFoundError{errors.New("no fork found")}

return results, nil
}

type RepoMetadataResult struct {
Expand Down
24 changes: 0 additions & 24 deletions context/blank_context.go

This file was deleted.

202 changes: 97 additions & 105 deletions context/context.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
// TODO: rename this package to avoid clash with stdlib
package context

import (
"errors"
"fmt"
"os"
"sort"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/prompt"
)

// Context represents the interface for querying information about the current environment
type Context interface {
Config() (config.Config, error)
}

// cap the number of git remotes looked up, since the user might have an
// unusually large number of git remotes
const maxRemotesForLookup = 5

// ResolveRemotesToRepos takes in a list of git remotes and fetches more information about the repositories they map to.
// Only the git remotes belonging to the same hostname are ever looked up; all others are ignored.
func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (ResolvedRemotes, error) {
func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (*ResolvedRemotes, error) {
sort.Stable(remotes)

result := ResolvedRemotes{
Remotes: remotes,
result := &ResolvedRemotes{
remotes: remotes,
apiClient: client,
}

Expand All @@ -38,138 +32,136 @@ func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (Re
if err != nil {
return result, err
}
result.BaseOverride = baseOverride
result.baseOverride = baseOverride
}

foundBaseOverride := false
var hostname string
return result, nil
}

func resolveNetwork(result *ResolvedRemotes) error {
var repos []ghrepo.Interface
for i, r := range remotes {
if i == 0 {
hostname = r.RepoHost()
} else if !strings.EqualFold(r.RepoHost(), hostname) {
// ignore all remotes for a hostname different to that of the 1st remote
continue
}
for _, r := range result.remotes {
repos = append(repos, r)
if baseOverride != nil && ghrepo.IsSame(r, baseOverride) {
foundBaseOverride = true
}
if len(repos) == maxRemotesForLookup {
break
}
}
if baseOverride != nil && !foundBaseOverride {
// additionally, look up the explicitly specified base repo if it's not
// already covered by git remotes
repos = append(repos, baseOverride)
}

networkResult, err := api.RepoNetwork(client, repos)
if err != nil {
return result, err
}
result.Network = networkResult
return result, nil
networkResult, err := api.RepoNetwork(result.apiClient, repos)
result.network = &networkResult
return err
}

type ResolvedRemotes struct {
BaseOverride ghrepo.Interface
Remotes Remotes
Network api.RepoNetworkResult
baseOverride ghrepo.Interface
remotes Remotes
network *api.RepoNetworkResult
apiClient *api.Client
}

// BaseRepo is the first found repository in the "upstream", "github", "origin"
// git remote order, resolved to the parent repo if the git remote points to a fork
func (r ResolvedRemotes) BaseRepo() (*api.Repository, error) {
if r.BaseOverride != nil {
for _, repo := range r.Network.Repositories {
if repo != nil && ghrepo.IsSame(repo, r.BaseOverride) {
return repo, nil
func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, error) {
if r.baseOverride != nil {
return r.baseOverride, nil
}

// if any of the remotes already has a resolution, respect that
for _, r := range r.remotes {
if r.Resolved == "base" {
return r, nil
} else if r.Resolved != "" {
repo, err := ghrepo.FromFullName(r.Resolved)
if err != nil {
return nil, err
}
return ghrepo.NewWithHost(repo.RepoOwner(), repo.RepoName(), r.RepoHost()), nil
}
}

if !io.CanPrompt() {
// we cannot prompt, so just resort to the 1st remote
return r.remotes[0], nil
}

// from here on, consult the API
if r.network == nil {
err := resolveNetwork(r)
if err != nil {
return nil, err
}
}

var repoNames []string
repoMap := map[string]*api.Repository{}
add := func(r *api.Repository) {
fn := ghrepo.FullName(r)
if _, ok := repoMap[fn]; !ok {
repoMap[fn] = r
repoNames = append(repoNames, fn)
}
return nil, fmt.Errorf("failed looking up information about the '%s' repository",
ghrepo.FullName(r.BaseOverride))
}

for _, repo := range r.Network.Repositories {
for _, repo := range r.network.Repositories {
if repo == nil {
continue
}
if repo.IsFork() {
return repo.Parent, nil
add(repo.Parent)
}
return repo, nil
add(repo)
}

return nil, errors.New("not found")
}

// HeadRepo is a fork of base repo (if any), or the first found repository that
// has push access
func (r ResolvedRemotes) HeadRepo() (*api.Repository, error) {
baseRepo, err := r.BaseRepo()
if err != nil {
return nil, err
if len(repoNames) == 0 {
return r.remotes[0], nil
}

// try to find a pushable fork among existing remotes
for _, repo := range r.Network.Repositories {
if repo != nil && repo.Parent != nil && repo.ViewerCanPush() && ghrepo.IsSame(repo.Parent, baseRepo) {
return repo, nil
baseName := repoNames[0]
if len(repoNames) > 1 {
err := prompt.SurveyAskOne(&survey.Select{
Message: "Which should be the base repository (used for e.g. querying issues) for this directory?",
Options: repoNames,
}, &baseName)
if err != nil {
return nil, err
}
}

// a fork might still exist on GitHub, so let's query for it
var notFound *api.NotFoundError
if repo, err := api.RepoFindFork(r.apiClient, baseRepo); err == nil {
return repo, nil
} else if !errors.As(err, &notFound) {
return nil, err
// determine corresponding git remote
selectedRepo := repoMap[baseName]
resolution := "base"
remote, _ := r.RemoteForRepo(selectedRepo)
if remote == nil {
remote = r.remotes[0]
resolution = ghrepo.FullName(selectedRepo)
}

// cache the result to git config
err := git.SetRemoteResolution(remote.Name, resolution)
return selectedRepo, err
}

func (r *ResolvedRemotes) HeadRepos() ([]*api.Repository, error) {
if r.network == nil {
err := resolveNetwork(r)
if err != nil {
return nil, err
}
}

// fall back to any listed repository that has push access
for _, repo := range r.Network.Repositories {
var results []*api.Repository
for _, repo := range r.network.Repositories {
if repo != nil && repo.ViewerCanPush() {
return repo, nil
results = append(results, repo)
}
}
return nil, errors.New("none of the repositories have push access")
return results, nil
}

// RemoteForRepo finds the git remote that points to a repository
func (r ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) {
for i, remote := range r.Remotes {
if ghrepo.IsSame(remote, repo) ||
// additionally, look up the resolved repository name in case this
// git remote points to this repository via a redirect
(r.Network.Repositories[i] != nil && ghrepo.IsSame(r.Network.Repositories[i], repo)) {
func (r *ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) {
for _, remote := range r.remotes {
if ghrepo.IsSame(remote, repo) {
return remote, nil
}
}
return nil, errors.New("not found")
}

// New initializes a Context that reads from the filesystem
func New() Context {
return &fsContext{}
}

// A Context implementation that queries the filesystem
type fsContext struct {
config config.Config
}

func (c *fsContext) Config() (config.Config, error) {
if c.config == nil {
cfg, err := config.ParseDefaultConfig()
if errors.Is(err, os.ErrNotExist) {
cfg = config.NewBlankConfig()
} else if err != nil {
return nil, err
}
c.config = cfg
}
return c.config, nil
}
Loading

0 comments on commit b2e36a0

Please sign in to comment.