cli/internal/ghrepo/repo.go
Mislav Marohnić c095a4bead Allow explicitly specifying the hostname for gh operations
Accept the "HOST/OWNER/REPO" syntax or passing a full URL for both the
`--repo` flag and the GH_REPO environment variable and allow setting
GH_HOST environment variable to override just the hostname for
operations that assume "github.com" by default.

Examples:

    $ gh repo clone example.org/owner/repo
    $ GH_HOST=example.org gh repo clone repo

    $ GH_HOST=example.org gh api user
    $ GH_HOST=example.org gh gist create myfile.txt

    $ gh issue list -R example.org/owner/repo
    $ gh issue list -R https://example.org/owner/repo.git
    $ GH_REPO=example.org/owner/repo gh issue list
2020-08-12 16:16:34 +02:00

123 lines
3.1 KiB
Go

package ghrepo
import (
"fmt"
"net/url"
"strings"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghinstance"
)
// Interface describes an object that represents a GitHub repository
type Interface interface {
RepoName() string
RepoOwner() string
RepoHost() string
}
// New instantiates a GitHub repository from owner and name arguments
func New(owner, repo string) Interface {
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: normalizeHostname(hostname),
}
}
// FullName serializes a GitHub repository into an "OWNER/REPO" string
func FullName(r Interface) string {
return fmt.Sprintf("%s/%s", r.RepoOwner(), r.RepoName())
}
// 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) {
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)
}
}
// FromURL extracts the GitHub repository information from a git remote URL
func FromURL(u *url.URL) (Interface, error) {
if u.Hostname() == "" {
return nil, fmt.Errorf("no hostname detected")
}
parts := strings.SplitN(strings.Trim(u.Path, "/"), "/", 3)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid path: %s", u.Path)
}
return NewWithHost(parts[0], strings.TrimSuffix(parts[1], ".git"), u.Hostname()), nil
}
func normalizeHostname(h string) string {
return strings.ToLower(strings.TrimPrefix(h, "www."))
}
// IsSame compares two GitHub repositories
func IsSame(a, b Interface) bool {
return strings.EqualFold(a.RepoOwner(), b.RepoOwner()) &&
strings.EqualFold(a.RepoName(), b.RepoName()) &&
normalizeHostname(a.RepoHost()) == normalizeHostname(b.RepoHost())
}
func GenerateRepoURL(repo Interface, p string, args ...interface{}) string {
baseURL := fmt.Sprintf("https://%s/%s/%s", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
if p != "" {
return baseURL + "/" + fmt.Sprintf(p, args...)
}
return baseURL
}
// TODO there is a parallel implementation for non-isolated commands
func FormatRemoteURL(repo Interface, protocol string) string {
if protocol == "ssh" {
return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
return fmt.Sprintf("https://%s/%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
type ghRepo struct {
owner string
name string
hostname string
}
func (r ghRepo) RepoOwner() string {
return r.owner
}
func (r ghRepo) RepoName() string {
return r.name
}
func (r ghRepo) RepoHost() string {
return r.hostname
}