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  }