Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for GH_HOST and expose repository.Parse #12

Merged
merged 2 commits into from
Jan 19, 2022
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
37 changes: 10 additions & 27 deletions gh.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import (
"errors"
"fmt"
"net/url"
"os"
"os/exec"

iapi "github.com/cli/go-gh/internal/api"
"github.com/cli/go-gh/internal/config"
"github.com/cli/go-gh/internal/git"
irepo "github.com/cli/go-gh/internal/repository"
"github.com/cli/go-gh/internal/ssh"
"github.com/cli/go-gh/pkg/api"
repo "github.com/cli/go-gh/pkg/repository"
"github.com/cli/safeexec"
)

Expand Down Expand Up @@ -109,7 +112,12 @@ func GQLClient(opts *api.ClientOptions) (api.GQLClient, error) {

// CurrentRepository uses git remotes to determine the GitHub repository
// the current directory is tracking.
func CurrentRepository() (Repository, error) {
func CurrentRepository() (repo.Repository, error) {
override := os.Getenv("GH_REPO")
if override != "" {
return repo.Parse(override)
}

remotes, err := git.Remotes()
if err != nil {
return nil, err
Expand All @@ -134,7 +142,7 @@ func CurrentRepository() (Repository, error) {
}

r := filteredRemotes[0]
return repo{host: r.Host, name: r.Repo, owner: r.Owner}, nil
return irepo.New(r.Host, r.Owner, r.Repo), nil
}

func translateRemotes(remotes git.RemoteSet, urlTranslate func(*url.URL) *url.URL) {
Expand All @@ -147,28 +155,3 @@ func translateRemotes(remotes git.RemoteSet, urlTranslate func(*url.URL) *url.UR
}
}
}

// Repository is the interface that wraps repository information methods.
type Repository interface {
Host() string
Name() string
Owner() string
}

type repo struct {
host string
name string
owner string
}

func (r repo) Host() string {
return r.host
}

func (r repo) Name() string {
return r.name
}

func (r repo) Owner() string {
return r.owner
}
4 changes: 2 additions & 2 deletions internal/git/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ func parseRemotes(gitRemotes []string) RemoteSet {
urlStr := strings.TrimSpace(match[2])
urlType := strings.TrimSpace(match[3])

url, err := parseURL(urlStr)
url, err := ParseURL(urlStr)
if err != nil {
continue
}
host, owner, repo, _ := repoInfoFromURL(url)
host, owner, repo, _ := RepoInfoFromURL(url)

var rem *Remote
if len(remotes) > 0 {
Expand Down
13 changes: 9 additions & 4 deletions internal/git/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"strings"
)

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

Expand All @@ -15,6 +15,7 @@ func isSupportedProtocol(u string) bool {
strings.HasPrefix(u, "git+ssh:") ||
strings.HasPrefix(u, "git:") ||
strings.HasPrefix(u, "http:") ||
strings.HasPrefix(u, "git+https:") ||
strings.HasPrefix(u, "https:")
}

Expand All @@ -25,8 +26,8 @@ func isPossibleProtocol(u string) bool {
strings.HasPrefix(u, "file:")
}

// Normalize git remote urls.
func parseURL(rawURL string) (u *url.URL, err error) {
// ParseURL normalizes git remote urls.
func ParseURL(rawURL string) (u *url.URL, err error) {
if !isPossibleProtocol(rawURL) &&
strings.ContainsRune(rawURL, ':') &&
// Not a Windows path.
Expand All @@ -44,6 +45,10 @@ func parseURL(rawURL string) (u *url.URL, err error) {
u.Scheme = "ssh"
}

if u.Scheme == "git+https" {
u.Scheme = "https"
}

if u.Scheme != "ssh" {
return
}
Expand All @@ -60,7 +65,7 @@ func parseURL(rawURL string) (u *url.URL, err error) {
}

// Extract GitHub repository information from a git remote URL.
func repoInfoFromURL(u *url.URL) (host string, owner string, repo string, err error) {
func RepoInfoFromURL(u *url.URL) (host string, owner string, name string, err error) {
mislav marked this conversation as resolved.
Show resolved Hide resolved
if u.Hostname() == "" {
return "", "", "", fmt.Errorf("no hostname detected")
}
Expand Down
36 changes: 33 additions & 3 deletions internal/git/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ func TestIsURL(t *testing.T) {
url: "git://example.com/owner/repo",
want: true,
},
{
name: "git with extension",
url: "git://example.com/owner/repo.git",
want: true,
},
{
name: "git+ssh",
url: "git+ssh://git@example.com/owner/repo.git",
want: true,
},
{
name: "git+https",
url: "git+https://example.com/owner/repo.git",
want: true,
},
{
name: "http",
url: "http://example.com/owner/repo.git",
want: true,
},
{
name: "https",
url: "https://example.com/owner/repo.git",
Expand All @@ -46,7 +66,7 @@ func TestIsURL(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isURL(tt.url))
assert.Equal(t, tt.want, IsURL(tt.url))
})
}
}
Expand Down Expand Up @@ -125,6 +145,16 @@ func TestParseURL(t *testing.T) {
Path: "/owner/repo.git",
},
},
{
name: "git+https",
url: "git+https://example.com/owner/repo.git",
want: url{
Scheme: "https",
User: "",
Host: "example.com",
Path: "/owner/repo.git",
},
},
{
name: "scp-like",
url: "git@example.com:owner/repo.git",
Expand Down Expand Up @@ -178,7 +208,7 @@ func TestParseURL(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u, err := parseURL(tt.url)
u, err := ParseURL(tt.url)
if tt.wantErr {
assert.Error(t, err)
return
Expand Down Expand Up @@ -275,7 +305,7 @@ func TestRepoInfoFromURL(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
u, err := url.Parse(tt.input)
assert.NoError(t, err)
host, owner, repo, err := repoInfoFromURL(u)
host, owner, repo, err := RepoInfoFromURL(u)
if tt.wantErr {
assert.EqualError(t, err, tt.wantErrMsg)
return
Expand Down
24 changes: 24 additions & 0 deletions internal/repository/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package repository

func New(host, owner, name string) repo {
return repo{host: host, owner: owner, name: name}
}

// Implements repository.Repository interface.
type repo struct {
host string
owner string
name string
}

func (r repo) Host() string {
return r.host
}

func (r repo) Owner() string {
return r.owner
}

func (r repo) Name() string {
return r.name
}
57 changes: 57 additions & 0 deletions pkg/repository/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Package repository is a set of types and functions for modeling and
// interacting with GitHub repositories.
package repository

import (
"fmt"
"os"
"strings"

"github.com/cli/go-gh/internal/git"
irepo "github.com/cli/go-gh/internal/repository"
)

// Repository is the interface that wraps repository information methods.
type Repository interface {
Host() string
Name() string
Owner() string
}

// Parse extracts the repository information from the following
// string formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL.
func Parse(s string) (Repository, error) {
if git.IsURL(s) {
u, err := git.ParseURL(s)
if err != nil {
return nil, err
}

host, owner, name, err := git.RepoInfoFromURL(u)
if err != nil {
return nil, err
}

return irepo.New(host, owner, name), nil
}

parts := strings.SplitN(s, "/", 4)
for _, p := range parts {
if len(p) == 0 {
return nil, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, s)
}
}

switch len(parts) {
case 3:
return irepo.New(parts[0], parts[1], parts[2]), nil
case 2:
host := os.Getenv("GH_HOST")
if host == "" {
host = "github.com"
}
return irepo.New(host, parts[0], parts[1]), nil
default:
return nil, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, s)
}
}
98 changes: 98 additions & 0 deletions pkg/repository/repository_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package repository

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestParse(t *testing.T) {
tests := []struct {
name string
input string
hostOverride string
wantOwner string
wantName string
wantHost string
wantErr string
}{
{
name: "OWNER/REPO combo",
input: "OWNER/REPO",
wantHost: "github.com",
wantOwner: "OWNER",
wantName: "REPO",
},
{
name: "too few elements",
input: "OWNER",
wantErr: `expected the "[HOST/]OWNER/REPO" format, got "OWNER"`,
},
{
name: "too many elements",
input: "a/b/c/d",
wantErr: `expected the "[HOST/]OWNER/REPO" format, got "a/b/c/d"`,
},
{
name: "blank value",
input: "a/",
wantErr: `expected the "[HOST/]OWNER/REPO" format, got "a/"`,
},
{
name: "with hostname",
input: "example.org/OWNER/REPO",
wantHost: "example.org",
wantOwner: "OWNER",
wantName: "REPO",
},
{
name: "full URL",
input: "https://example.org/OWNER/REPO.git",
wantHost: "example.org",
wantOwner: "OWNER",
wantName: "REPO",
},
{
name: "SSH URL",
input: "git@example.org:OWNER/REPO.git",
wantHost: "example.org",
wantOwner: "OWNER",
wantName: "REPO",
},
{
name: "OWNER/REPO with default host override",
input: "OWNER/REPO",
hostOverride: "override.com",
wantHost: "override.com",
wantOwner: "OWNER",
wantName: "REPO",
},
{
name: "HOST/OWNER/REPO with default host override",
input: "example.com/OWNER/REPO",
hostOverride: "override.com",
wantHost: "example.com",
wantOwner: "OWNER",
wantName: "REPO",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.hostOverride != "" {
old := os.Getenv("GH_HOST")
os.Setenv("GH_HOST", tt.hostOverride)
defer os.Setenv("GH_HOST", old)
}
r, err := Parse(tt.input)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantHost, r.Host())
assert.Equal(t, tt.wantOwner, r.Owner())
assert.Equal(t, tt.wantName, r.Name())
})
}
}