Before: the default push target for the current branch in `pr create` was the first repository found among git remotes that has write access. Now: the default push target is the fork the base repo, if said fork exists and has write access, falling back to old behavior otherwise. This change in the default is to facilitate contributions to projects that have a hard requirement that all pull requests (even those opened by people with write access to that project) come from forks.
264 lines
6.2 KiB
Go
264 lines
6.2 KiB
Go
package context
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
|
|
"github.com/cli/cli/api"
|
|
"github.com/cli/cli/git"
|
|
"github.com/cli/cli/internal/ghrepo"
|
|
"github.com/mitchellh/go-homedir"
|
|
)
|
|
|
|
// Context represents the interface for querying information about the current environment
|
|
type Context interface {
|
|
AuthToken() (string, error)
|
|
SetAuthToken(string)
|
|
AuthLogin() (string, error)
|
|
Branch() (string, error)
|
|
SetBranch(string)
|
|
Remotes() (Remotes, error)
|
|
BaseRepo() (ghrepo.Interface, error)
|
|
SetBaseRepo(string)
|
|
}
|
|
|
|
// cap the number of git remotes looked up, since the user might have an
|
|
// unusually large number of git remotes
|
|
const maxRemotesForLookup = 5
|
|
|
|
func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (ResolvedRemotes, error) {
|
|
sort.Stable(remotes)
|
|
lenRemotesForLookup := len(remotes)
|
|
if lenRemotesForLookup > maxRemotesForLookup {
|
|
lenRemotesForLookup = maxRemotesForLookup
|
|
}
|
|
|
|
hasBaseOverride := base != ""
|
|
baseOverride := ghrepo.FromFullName(base)
|
|
foundBaseOverride := false
|
|
repos := make([]ghrepo.Interface, 0, lenRemotesForLookup)
|
|
for _, r := range remotes[:lenRemotesForLookup] {
|
|
repos = append(repos, r)
|
|
if ghrepo.IsSame(r, baseOverride) {
|
|
foundBaseOverride = true
|
|
}
|
|
}
|
|
if hasBaseOverride && !foundBaseOverride {
|
|
// additionally, look up the explicitly specified base repo if it's not
|
|
// already covered by git remotes
|
|
repos = append(repos, baseOverride)
|
|
}
|
|
|
|
result := ResolvedRemotes{
|
|
Remotes: remotes,
|
|
apiClient: client,
|
|
}
|
|
if hasBaseOverride {
|
|
result.BaseOverride = baseOverride
|
|
}
|
|
networkResult, err := api.RepoNetwork(client, repos)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Network = networkResult
|
|
return result, nil
|
|
}
|
|
|
|
type ResolvedRemotes struct {
|
|
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
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("failed looking up information about the '%s' repository",
|
|
ghrepo.FullName(r.BaseOverride))
|
|
}
|
|
|
|
for _, repo := range r.Network.Repositories {
|
|
if repo == nil {
|
|
continue
|
|
}
|
|
if repo.IsFork() {
|
|
return repo.Parent, nil
|
|
}
|
|
return repo, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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, ¬Found) {
|
|
return nil, err
|
|
}
|
|
|
|
// fall back to any listed repository that has push access
|
|
for _, repo := range r.Network.Repositories {
|
|
if repo != nil && repo.ViewerCanPush() {
|
|
return repo, nil
|
|
}
|
|
}
|
|
return nil, errors.New("none of the repositories have push access")
|
|
}
|
|
|
|
// 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)) {
|
|
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 *configEntry
|
|
remotes Remotes
|
|
branch string
|
|
baseRepo ghrepo.Interface
|
|
authToken string
|
|
}
|
|
|
|
func ConfigDir() string {
|
|
dir, _ := homedir.Expand("~/.config/gh")
|
|
return dir
|
|
}
|
|
|
|
func configFile() string {
|
|
return path.Join(ConfigDir(), "config.yml")
|
|
}
|
|
|
|
func (c *fsContext) getConfig() (*configEntry, error) {
|
|
if c.config == nil {
|
|
entry, err := parseOrSetupConfigFile(configFile())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.config = entry
|
|
c.authToken = ""
|
|
}
|
|
return c.config, nil
|
|
}
|
|
|
|
func (c *fsContext) AuthToken() (string, error) {
|
|
if c.authToken != "" {
|
|
return c.authToken, nil
|
|
}
|
|
|
|
config, err := c.getConfig()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return config.Token, nil
|
|
}
|
|
|
|
func (c *fsContext) SetAuthToken(t string) {
|
|
c.authToken = t
|
|
}
|
|
|
|
func (c *fsContext) AuthLogin() (string, error) {
|
|
config, err := c.getConfig()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return config.User, nil
|
|
}
|
|
|
|
func (c *fsContext) Branch() (string, error) {
|
|
if c.branch != "" {
|
|
return c.branch, nil
|
|
}
|
|
|
|
currentBranch, err := git.CurrentBranch()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
c.branch = currentBranch
|
|
return c.branch, nil
|
|
}
|
|
|
|
func (c *fsContext) SetBranch(b string) {
|
|
c.branch = b
|
|
}
|
|
|
|
func (c *fsContext) Remotes() (Remotes, error) {
|
|
if c.remotes == nil {
|
|
gitRemotes, err := git.Remotes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(gitRemotes) == 0 {
|
|
return nil, errors.New("no git remotes found")
|
|
}
|
|
|
|
sshTranslate := git.ParseSSHConfig().Translator()
|
|
c.remotes = translateRemotes(gitRemotes, sshTranslate)
|
|
}
|
|
|
|
if len(c.remotes) == 0 {
|
|
return nil, errors.New("no git remote found for a github.com repository")
|
|
}
|
|
return c.remotes, nil
|
|
}
|
|
|
|
func (c *fsContext) BaseRepo() (ghrepo.Interface, error) {
|
|
if c.baseRepo != nil {
|
|
return c.baseRepo, nil
|
|
}
|
|
|
|
remotes, err := c.Remotes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rem, err := remotes.FindByName("upstream", "github", "origin", "*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.baseRepo = rem
|
|
return c.baseRepo, nil
|
|
}
|
|
|
|
func (c *fsContext) SetBaseRepo(nwo string) {
|
|
c.baseRepo = ghrepo.FromFullName(nwo)
|
|
}
|