Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/gh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/cli/cli/command"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/run"
"github.com/cli/cli/pkg/cmd/alias/expand"
"github.com/cli/cli/pkg/cmd/factory"
Expand All @@ -35,6 +36,10 @@ func main() {

hasDebug := os.Getenv("DEBUG") != ""

if hostFromEnv := os.Getenv("GH_HOST"); hostFromEnv != "" {
ghinstance.OverrideDefault(hostFromEnv)
}

cmdFactory := factory.New(command.Version)
stderr := cmdFactory.IOStreams.ErrOut
rootCmd := root.NewCmdRoot(cmdFactory, command.Version, command.BuildDate)
Expand Down
2 changes: 1 addition & 1 deletion context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (Re
continue
}
repos = append(repos, r)
if ghrepo.IsSame(r, baseOverride) {
if baseOverride != nil && ghrepo.IsSame(r, baseOverride) {
foundBaseOverride = true
}
if len(repos) == maxRemotesForLookup {
Expand Down
9 changes: 9 additions & 0 deletions git/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`)
// RemoteSet is a slice of git remotes
type RemoteSet []*Remote

func NewRemote(name string, u string) *Remote {
pu, _ := url.Parse(u)
return &Remote{
Name: name,
FetchURL: pu,
PushURL: pu,
}
}

// Remote is a parsed git remote
type Remote struct {
Name string
Expand Down
4 changes: 4 additions & 0 deletions git/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ var (
protocolRe = regexp.MustCompile("^[a-zA-Z_+-]+://")
)

func IsURL(u string) bool {
return strings.HasPrefix(u, "git@") || protocolRe.MatchString(u)
}

// ParseURL normalizes git remote urls
func ParseURL(rawURL string) (u *url.URL, err error) {
if !protocolRe.MatchString(rawURL) &&
Expand Down
25 changes: 18 additions & 7 deletions internal/config/config_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package config

import (
"bytes"
"errors"
"fmt"
"reflect"
"testing"
Expand Down Expand Up @@ -71,17 +70,29 @@ github.com:
eq(t, token, "OTOKEN")
}

func Test_parseConfig_notFound(t *testing.T) {
func Test_parseConfig_hostFallback(t *testing.T) {
defer StubConfig(`---
hosts:
example.com:
git_protocol: ssh
`, `---
github.com:
user: monalisa
oauth_token: OTOKEN
example.com:
user: wronguser
oauth_token: NOTTHIS
`, "")()
git_protocol: https
`)()
config, err := ParseConfig("config.yml")
eq(t, err, nil)
_, err = config.Get("github.com", "user")
eq(t, err, &NotFoundError{errors.New(`could not find config entry for "github.com"`)})
val, err := config.Get("example.com", "git_protocol")
eq(t, err, nil)
eq(t, val, "https")
val, err = config.Get("github.com", "git_protocol")
eq(t, err, nil)
eq(t, val, "ssh")
val, err = config.Get("nonexist.io", "git_protocol")
eq(t, err, nil)
eq(t, val, "ssh")
}

func Test_ParseConfig_migrateConfig(t *testing.T) {
Expand Down
17 changes: 10 additions & 7 deletions internal/config/config_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,21 @@ func (c *fileConfig) Root() *yaml.Node {

func (c *fileConfig) Get(hostname, key string) (string, error) {
if hostname != "" {
hostCfg, err := c.configForHost(hostname)
if err != nil {
return "", err
}

hostValue, err := hostCfg.GetStringValue(key)
var notFound *NotFoundError

hostCfg, err := c.configForHost(hostname)
if err != nil && !errors.As(err, &notFound) {
return "", err
}

var hostValue string
if hostCfg != nil {
hostValue, err = hostCfg.GetStringValue(key)
if err != nil && !errors.As(err, &notFound) {
return "", err
}
}

if hostValue != "" {
return hostValue, nil
}
Expand Down Expand Up @@ -385,7 +388,7 @@ func (c *fileConfig) hostEntries() ([]*HostConfig, error) {
return hostConfigs, nil
}

// Hosts returns a list of all known hostnames configred in hosts.yml
// Hosts returns a list of all known hostnames configured in hosts.yml
func (c *fileConfig) Hosts() ([]string, error) {
entries, err := c.hostEntries()
if err != nil {
Expand Down
16 changes: 16 additions & 0 deletions internal/ghinstance/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,27 @@ import (

const defaultHostname = "github.com"

var hostnameOverride string

// Default returns the host name of the default GitHub instance
func Default() string {
return defaultHostname
}

// OverridableDefault is like Default, except it is overridable by the GH_HOST environment variable
func OverridableDefault() string {
if hostnameOverride != "" {
return hostnameOverride
}
return defaultHostname
}

// OverrideDefault overrides the value returned from OverridableDefault. This should only ever be
// called from the main runtime path, not tests.
func OverrideDefault(newhost string) {
hostnameOverride = newhost
}

// IsEnterprise reports whether a non-normalized host name looks like a GHE instance
func IsEnterprise(h string) bool {
return NormalizeHostname(h) != defaultHostname
Expand Down
23 changes: 23 additions & 0 deletions internal/ghinstance/host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ import (
"testing"
)

func TestOverridableDefault(t *testing.T) {
oldOverride := hostnameOverride
t.Cleanup(func() {
hostnameOverride = oldOverride
})

host := OverridableDefault()
if host != "github.com" {
t.Errorf("expected github.com, got %q", host)
}

OverrideDefault("example.org")

host = OverridableDefault()
if host != "example.org" {
t.Errorf("expected example.org, got %q", host)
}
host = Default()
if host != "github.com" {
t.Errorf("expected github.com, got %q", host)
}
}

func TestIsEnterprise(t *testing.T) {
tests := []struct {
host string
Expand Down
53 changes: 30 additions & 23 deletions internal/ghrepo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"fmt"
"net/url"
"strings"
)

const defaultHostname = "github.com"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghinstance"
)

// Interface describes an object that represents a GitHub repository
type Interface interface {
Expand All @@ -17,18 +18,15 @@ type Interface interface {

// New instantiates a GitHub repository from owner and name arguments
func New(owner, repo string) Interface {
return &ghRepo{
owner: owner,
name: repo,
}
return NewWithHost(owner, repo, ghinstance.OverridableDefault())
}

// NewWithHost is like New with an explicit host name
func NewWithHost(owner, repo, hostname string) Interface {
return &ghRepo{
owner: owner,
name: repo,
hostname: hostname,
hostname: normalizeHostname(hostname),
}
}

Expand All @@ -37,15 +35,31 @@ func FullName(r Interface) string {
return fmt.Sprintf("%s/%s", r.RepoOwner(), r.RepoName())
}

// FromFullName extracts the GitHub repository information from an "OWNER/REPO" string
// FromFullName extracts the GitHub repository information from the following
// formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL.
func FromFullName(nwo string) (Interface, error) {
var r ghRepo
parts := strings.SplitN(nwo, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return &r, fmt.Errorf("expected OWNER/REPO format, got %q", nwo)
if git.IsURL(nwo) {
u, err := git.ParseURL(nwo)
if err != nil {
return nil, err
}
return FromURL(u)
}

parts := strings.SplitN(nwo, "/", 4)
for _, p := range parts {
if len(p) == 0 {
return nil, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, nwo)
}
}
switch len(parts) {
case 3:
return NewWithHost(parts[1], parts[2], normalizeHostname(parts[0])), nil
case 2:
return New(parts[0], parts[1]), nil
default:
return nil, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, nwo)
}
r.owner, r.name = parts[0], parts[1]
return &r, nil
}

// FromURL extracts the GitHub repository information from a git remote URL
Expand All @@ -59,11 +73,7 @@ func FromURL(u *url.URL) (Interface, error) {
return nil, fmt.Errorf("invalid path: %s", u.Path)
}

return &ghRepo{
owner: parts[0],
name: strings.TrimSuffix(parts[1], ".git"),
hostname: normalizeHostname(u.Hostname()),
}, nil
return NewWithHost(parts[0], strings.TrimSuffix(parts[1], ".git"), u.Hostname()), nil
}

func normalizeHostname(h string) string {
Expand Down Expand Up @@ -109,8 +119,5 @@ func (r ghRepo) RepoName() string {
}

func (r ghRepo) RepoHost() string {
if r.hostname != "" {
return r.hostname
}
return defaultHostname
return r.hostname
}
84 changes: 84 additions & 0 deletions internal/ghrepo/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,87 @@ func Test_repoFromURL(t *testing.T) {
})
}
}

func TestFromFullName(t *testing.T) {
tests := []struct {
name string
input string
wantOwner string
wantName string
wantHost string
wantErr error
}{
{
name: "OWNER/REPO combo",
input: "OWNER/REPO",
wantHost: "github.com",
wantOwner: "OWNER",
wantName: "REPO",
wantErr: nil,
},
{
name: "too few elements",
input: "OWNER",
wantErr: errors.New(`expected the "[HOST/]OWNER/REPO" format, got "OWNER"`),
},
{
name: "too many elements",
input: "a/b/c/d",
wantErr: errors.New(`expected the "[HOST/]OWNER/REPO" format, got "a/b/c/d"`),
},
{
name: "blank value",
input: "a/",
wantErr: errors.New(`expected the "[HOST/]OWNER/REPO" format, got "a/"`),
},
{
name: "with hostname",
input: "example.org/OWNER/REPO",
wantHost: "example.org",
wantOwner: "OWNER",
wantName: "REPO",
wantErr: nil,
},
{
name: "full URL",
input: "https://example.org/OWNER/REPO.git",
wantHost: "example.org",
wantOwner: "OWNER",
wantName: "REPO",
wantErr: nil,
},
{
name: "SSH URL",
input: "[email protected]:OWNER/REPO.git",
wantHost: "example.org",
wantOwner: "OWNER",
wantName: "REPO",
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, err := FromFullName(tt.input)
if tt.wantErr != nil {
if err == nil {
t.Fatalf("no error in result, expected %v", tt.wantErr)
} else if err.Error() != tt.wantErr.Error() {
t.Fatalf("expected error %q, got %q", tt.wantErr.Error(), err.Error())
}
return
}
if err != nil {
t.Fatalf("got error %v", err)
}
if r.RepoHost() != tt.wantHost {
t.Errorf("expected host %q, got %q", tt.wantHost, r.RepoHost())
}
if r.RepoOwner() != tt.wantOwner {
t.Errorf("expected owner %q, got %q", tt.wantOwner, r.RepoOwner())
}
if r.RepoName() != tt.wantName {
t.Errorf("expected name %q, got %q", tt.wantName, r.RepoName())
}
})
}
}
5 changes: 4 additions & 1 deletion pkg/cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"

"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
Expand Down Expand Up @@ -195,9 +196,11 @@ func apiRun(opts *ApiOptions) error {
opts.IO.Out = ioutil.Discard
}

host := ghinstance.OverridableDefault()

hasNextPage := true
for hasNextPage {
resp, err := httpRequest(httpClient, method, requestPath, requestBody, requestHeaders)
resp, err := httpRequest(httpClient, host, method, requestPath, requestBody, requestHeaders)
if err != nil {
return err
}
Expand Down
Loading