Merge pull request #1258 from cli/ghe-remotes

Parse and respect non-github.com git remotes
This commit is contained in:
Mislav Marohnić 2020-07-15 13:07:30 +02:00 committed by GitHub
commit c8cf54c10c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 288 additions and 145 deletions

View file

@ -34,6 +34,9 @@ type Repository struct {
}
Parent *Repository
// pseudo-field that keeps track of host name of this repo
hostname string
}
// RepositoryOwner is the owner of a GitHub repository
@ -51,6 +54,11 @@ func (r Repository) RepoName() string {
return r.Name
}
// RepoHost is the GitHub hostname of the repository
func (r Repository) RepoHost() string {
return r.hostname
}
// IsFork is true when this repository has a parent repository
func (r Repository) IsFork() bool {
return r.Parent != nil
@ -103,7 +111,7 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
return nil, err
}
return &result.Repository, nil
return initRepoHostname(&result.Repository, repo.RepoHost()), nil
}
// RepoParent finds out the parent repository of a fork
@ -133,7 +141,7 @@ func RepoParent(client *Client, repo ghrepo.Interface) (ghrepo.Interface, error)
return nil, nil
}
parent := ghrepo.New(query.Repository.Parent.Owner.Login, query.Repository.Parent.Name)
parent := ghrepo.NewWithHost(query.Repository.Parent.Owner.Login, query.Repository.Parent.Name, repo.RepoHost())
return parent, nil
}
@ -145,6 +153,11 @@ type RepoNetworkResult struct {
// RepoNetwork inspects the relationship between multiple GitHub repositories
func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, error) {
var hostname string
if len(repos) > 0 {
hostname = repos[0].RepoHost()
}
queries := make([]string, 0, len(repos))
for i, repo := range repos {
queries = append(queries, fmt.Sprintf(`
@ -227,7 +240,7 @@ func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, e
if err := decoder.Decode(&repo); err != nil {
return result, err
}
result.Repositories = append(result.Repositories, &repo)
result.Repositories = append(result.Repositories, initRepoHostname(&repo, hostname))
} else {
return result, fmt.Errorf("unknown GraphQL result key %q", name)
}
@ -235,6 +248,14 @@ func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, e
return result, nil
}
func initRepoHostname(repo *Repository, hostname string) *Repository {
repo.hostname = hostname
if repo.Parent != nil {
repo.Parent.hostname = hostname
}
return repo
}
// repositoryV3 is the repository result from GitHub API v3
type repositoryV3 struct {
NodeID string
@ -265,6 +286,7 @@ func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
Login: result.Owner.Login,
},
ViewerPermission: "WRITE",
hostname: repo.RepoHost(),
}, nil
}
@ -306,7 +328,7 @@ func RepoFindFork(client *Client, repo ghrepo.Interface) (*Repository, error) {
// `affiliations` condition, to guard against versions of GitHub with a
// faulty `affiliations` implementation
if len(forks) > 0 && forks[0].ViewerCanPush() {
return &forks[0], nil
return initRepoHostname(&forks[0], repo.RepoHost()), nil
}
return nil, &NotFoundError{errors.New("no fork found")}
}
@ -368,7 +390,8 @@ func RepoCreate(client *Client, input RepoCreateInput) (*Repository, error) {
return nil, err
}
return &response.CreateRepository.Repository, nil
// FIXME: support Enterprise hosts
return initRepoHostname(&response.CreateRepository.Repository, "github.com"), nil
}
func RepositoryReadme(client *Client, fullName string) (string, error) {

View file

@ -1,11 +1,9 @@
package command
import (
"errors"
"fmt"
"io"
"net/url"
"regexp"
"strconv"
"strings"
"time"
@ -259,12 +257,7 @@ func issueView(cmd *cobra.Command, args []string) error {
return err
}
baseRepo, err := determineBaseRepo(apiClient, cmd, ctx)
if err != nil {
return err
}
issue, err := issueFromArg(apiClient, baseRepo, args[0])
issue, _, err := issueFromArg(ctx, apiClient, cmd, args[0])
if err != nil {
return err
}
@ -380,21 +373,6 @@ func printHumanIssuePreview(out io.Writer, issue *api.Issue) error {
return nil
}
var issueURLRE = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/issues/(\d+)`)
func issueFromArg(apiClient *api.Client, baseRepo ghrepo.Interface, arg string) (*api.Issue, error) {
if issueNumber, err := strconv.Atoi(strings.TrimPrefix(arg, "#")); err == nil {
return api.IssueByNumber(apiClient, baseRepo, issueNumber)
}
if m := issueURLRE.FindStringSubmatch(arg); m != nil {
issueNumber, _ := strconv.Atoi(m[3])
return api.IssueByNumber(apiClient, baseRepo, issueNumber)
}
return nil, fmt.Errorf("invalid issue format: %q", arg)
}
func issueCreate(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
apiClient, err := apiClientForContext(ctx)
@ -450,8 +428,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
}
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
// TODO: move URL generation into GitHubRepository
openURL := fmt.Sprintf("https://github.com/%s/issues/new", ghrepo.FullName(baseRepo))
openURL := generateRepoURL(baseRepo, "issues/new")
if title != "" || body != "" {
milestone := ""
if len(milestoneTitles) > 0 {
@ -527,7 +504,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
}
if action == PreviewAction {
openURL := fmt.Sprintf("https://github.com/%s/issues/new/", ghrepo.FullName(baseRepo))
openURL := generateRepoURL(baseRepo, "issues/new")
milestone := ""
if len(milestoneTitles) > 0 {
milestone = milestoneTitles[0]
@ -563,6 +540,14 @@ func issueCreate(cmd *cobra.Command, args []string) error {
return nil
}
func generateRepoURL(repo ghrepo.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
}
func addMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *issueMetadataState) error {
if !tb.HasMetadata() {
return nil
@ -735,19 +720,11 @@ func issueClose(cmd *cobra.Command, args []string) error {
return err
}
baseRepo, err := determineBaseRepo(apiClient, cmd, ctx)
issue, baseRepo, err := issueFromArg(ctx, apiClient, cmd, args[0])
if err != nil {
return err
}
issue, err := issueFromArg(apiClient, baseRepo, args[0])
var idErr *api.IssuesDisabledError
if errors.As(err, &idErr) {
return fmt.Errorf("issues disabled for %s", ghrepo.FullName(baseRepo))
} else if err != nil {
return err
}
if issue.Closed {
fmt.Fprintf(colorableErr(cmd), "%s Issue #%d (%s) is already closed\n", utils.Yellow("!"), issue.Number, issue.Title)
return nil
@ -755,7 +732,7 @@ func issueClose(cmd *cobra.Command, args []string) error {
err = api.IssueClose(apiClient, baseRepo, *issue)
if err != nil {
return fmt.Errorf("API call failed:%w", err)
return err
}
fmt.Fprintf(colorableErr(cmd), "%s Closed issue #%d (%s)\n", utils.Red("✔"), issue.Number, issue.Title)
@ -770,19 +747,11 @@ func issueReopen(cmd *cobra.Command, args []string) error {
return err
}
baseRepo, err := determineBaseRepo(apiClient, cmd, ctx)
issue, baseRepo, err := issueFromArg(ctx, apiClient, cmd, args[0])
if err != nil {
return err
}
issue, err := issueFromArg(apiClient, baseRepo, args[0])
var idErr *api.IssuesDisabledError
if errors.As(err, &idErr) {
return fmt.Errorf("issues disabled for %s", ghrepo.FullName(baseRepo))
} else if err != nil {
return err
}
if !issue.Closed {
fmt.Fprintf(colorableErr(cmd), "%s Issue #%d (%s) is already open\n", utils.Yellow("!"), issue.Number, issue.Title)
return nil
@ -790,7 +759,7 @@ func issueReopen(cmd *cobra.Command, args []string) error {
err = api.IssueReopen(apiClient, baseRepo, *issue)
if err != nil {
return fmt.Errorf("API call failed:%w", err)
return err
}
fmt.Fprintf(colorableErr(cmd), "%s Reopened issue #%d (%s)\n", utils.Green("✔"), issue.Number, issue.Title)

64
command/issue_lookup.go Normal file
View file

@ -0,0 +1,64 @@
package command
import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/ghrepo"
"github.com/spf13/cobra"
)
func issueFromArg(ctx context.Context, apiClient *api.Client, cmd *cobra.Command, arg string) (*api.Issue, ghrepo.Interface, error) {
issue, baseRepo, err := issueFromURL(apiClient, arg)
if err != nil {
return nil, nil, err
}
if issue != nil {
return issue, baseRepo, nil
}
baseRepo, err = determineBaseRepo(apiClient, cmd, ctx)
if err != nil {
return nil, nil, fmt.Errorf("could not determine base repo: %w", err)
}
issueNumber, err := strconv.Atoi(strings.TrimPrefix(arg, "#"))
if err != nil {
return nil, nil, fmt.Errorf("invalid issue format: %q", arg)
}
issue, err = issueFromNumber(apiClient, baseRepo, issueNumber)
return issue, baseRepo, err
}
var issueURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/issues/(\d+)`)
func issueFromURL(apiClient *api.Client, s string) (*api.Issue, ghrepo.Interface, error) {
u, err := url.Parse(s)
if err != nil {
return nil, nil, nil
}
if u.Scheme != "https" && u.Scheme != "http" {
return nil, nil, nil
}
m := issueURLRE.FindStringSubmatch(u.Path)
if m == nil {
return nil, nil, nil
}
repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
issueNumber, _ := strconv.Atoi(m[3])
issue, err := issueFromNumber(apiClient, repo, issueNumber)
return issue, repo, err
}
func issueFromNumber(apiClient *api.Client, repo ghrepo.Interface, issueNumber int) (*api.Issue, error) {
return api.IssueByNumber(apiClient, repo, issueNumber)
}

View file

@ -473,6 +473,7 @@ func TestIssueView_tty_Preview(t *testing.T) {
func TestIssueView_web_notFound(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "errors": [
@ -518,7 +519,6 @@ func TestIssueView_disabledIssues(t *testing.T) {
func TestIssueView_web_urlArg(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.StubResponse(200, bytes.NewBufferString(`
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
@ -965,12 +965,8 @@ func TestIssueClose_issuesDisabled(t *testing.T) {
`))
_, err := RunCommand("issue close 13")
if err == nil {
t.Fatalf("expected error when issues are disabled")
}
if !strings.Contains(err.Error(), "issues disabled") {
t.Fatalf("got unexpected error: %s", err)
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Fatalf("got error: %v", err)
}
}
@ -1038,11 +1034,7 @@ func TestIssueReopen_issuesDisabled(t *testing.T) {
`))
_, err := RunCommand("issue reopen 2")
if err == nil {
t.Fatalf("expected error when issues are disabled")
}
if !strings.Contains(err.Error(), "issues disabled") {
t.Fatalf("got unexpected error: %s", err)
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
t.Fatalf("got error: %v", err)
}
}

View file

@ -33,7 +33,7 @@ func prCheckout(cmd *cobra.Command, args []string) error {
baseRemote, _ := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
// baseRemoteSpec is a repository URL or a remote name to be used in git fetch
baseURLOrName := formatRemoteURL(cmd, ghrepo.FullName(baseRepo))
baseURLOrName := formatRemoteURL(cmd, baseRepo)
if baseRemote != nil {
baseURLOrName = baseRemote.Name
}
@ -84,7 +84,8 @@ func prCheckout(cmd *cobra.Command, args []string) error {
remote := baseURLOrName
mergeRef := ref
if pr.MaintainerCanModify {
remote = formatRemoteURL(cmd, fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name))
headRepo := ghrepo.NewWithHost(pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name, baseRepo.RepoHost())
remote = formatRemoteURL(cmd, headRepo)
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
}
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {

View file

@ -295,7 +295,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
// In either case, we want to add the head repo as a new git remote so we
// can push to it.
if headRemote == nil {
headRepoURL := formatRemoteURL(cmd, ghrepo.FullName(headRepo))
headRepoURL := formatRemoteURL(cmd, headRepo)
// TODO: prevent clashes with another remote of a same name
gitRemote, err := git.AddRemote("fork", headRepoURL)
@ -304,8 +304,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
}
headRemote = &context.Remote{
Remote: gitRemote,
Owner: headRepo.RepoOwner(),
Repo: headRepo.RepoName(),
Repo: headRepo,
}
}
@ -438,12 +437,7 @@ func withPrAndIssueQueryParams(baseURL, title, body string, assignees, labels, p
}
func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assignees, labels, projects []string, milestone string) (string, error) {
u := fmt.Sprintf(
"https://github.com/%s/compare/%s...%s?expand=1",
ghrepo.FullName(r),
base,
head,
)
u := generateRepoURL(r, "compare/%s...%s?expand=1", base, head)
url, err := withPrAndIssueQueryParams(u, title, body, assignees, labels, projects, milestone)
if err != nil {
return "", err

View file

@ -9,6 +9,7 @@ import (
"github.com/cli/cli/context"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/test"
)
@ -759,13 +760,11 @@ func Test_determineTrackingBranch_noMatch(t *testing.T) {
remotes := context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "hubot",
Repo: "Spoon-Knife",
Repo: ghrepo.New("hubot", "Spoon-Knife"),
},
&context.Remote{
Remote: &git.Remote{Name: "upstream"},
Owner: "octocat",
Repo: "Spoon-Knife",
Repo: ghrepo.New("octocat", "Spoon-Knife"),
},
}
@ -786,13 +785,11 @@ func Test_determineTrackingBranch_hasMatch(t *testing.T) {
remotes := context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "hubot",
Repo: "Spoon-Knife",
Repo: ghrepo.New("hubot", "Spoon-Knife"),
},
&context.Remote{
Remote: &git.Remote{Name: "upstream"},
Owner: "octocat",
Repo: "Spoon-Knife",
Repo: ghrepo.New("octocat", "Spoon-Knife"),
},
}
@ -819,8 +816,7 @@ func Test_determineTrackingBranch_respectTrackingConfig(t *testing.T) {
remotes := context.Remotes{
&context.Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "hubot",
Repo: "Spoon-Knife",
Repo: ghrepo.New("hubot", "Spoon-Knife"),
},
}

View file

@ -2,6 +2,7 @@ package command
import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
@ -55,16 +56,27 @@ func prFromNumberString(ctx context.Context, apiClient *api.Client, repo ghrepo.
return nil, nil
}
var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`)
func prFromURL(ctx context.Context, apiClient *api.Client, s string) (*api.PullRequest, ghrepo.Interface, error) {
r := regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/pull/(\d+)`)
if m := r.FindStringSubmatch(s); m != nil {
repo := ghrepo.New(m[1], m[2])
prNumberString := m[3]
pr, err := prFromNumberString(ctx, apiClient, repo, prNumberString)
return pr, repo, err
u, err := url.Parse(s)
if err != nil {
return nil, nil, nil
}
return nil, nil, nil
if u.Scheme != "https" && u.Scheme != "http" {
return nil, nil, nil
}
m := pullURLRE.FindStringSubmatch(u.Path)
if m == nil {
return nil, nil, nil
}
repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
prNumberString := m[3]
pr, err := prFromNumberString(ctx, apiClient, repo, prNumberString)
return pr, repo, err
}
func prForCurrentBranch(ctx context.Context, apiClient *api.Client, repo ghrepo.Interface) (*api.PullRequest, error) {

View file

@ -186,7 +186,11 @@ func repoClone(cmd *cobra.Command, args []string) error {
}
cloneURL = currentUser + "/" + cloneURL
}
cloneURL = formatRemoteURL(cmd, cloneURL)
repo, err := ghrepo.FromFullName(cloneURL)
if err != nil {
return err
}
cloneURL = formatRemoteURL(cmd, repo)
}
var repo ghrepo.Interface
@ -221,7 +225,7 @@ func repoClone(cmd *cobra.Command, args []string) error {
}
func addUpstreamRemote(cmd *cobra.Command, parentRepo ghrepo.Interface, cloneDir string) error {
upstreamURL := formatRemoteURL(cmd, ghrepo.FullName(parentRepo))
upstreamURL := formatRemoteURL(cmd, parentRepo)
cloneCmd := git.GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL)
cloneCmd.Stdout = os.Stdout
@ -322,7 +326,7 @@ func repoCreate(cmd *cobra.Command, args []string) error {
fmt.Fprintln(out, repo.URL)
}
remoteURL := formatRemoteURL(cmd, ghrepo.FullName(repo))
remoteURL := formatRemoteURL(cmd, repo)
if projectDirErr == nil {
_, err = git.AddRemote("origin", remoteURL)
@ -497,7 +501,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
fmt.Fprintf(out, "%s Renamed %s remote to %s\n", greenCheck, utils.Bold(remoteName), utils.Bold(renameTarget))
}
forkedRepoCloneURL := formatRemoteURL(cmd, ghrepo.FullName(forkedRepo))
forkedRepoCloneURL := formatRemoteURL(cmd, forkedRepo)
_, err = git.AddRemote(remoteName, forkedRepoCloneURL)
if err != nil {
@ -515,7 +519,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
}
}
if cloneDesired {
forkedRepoCloneURL := formatRemoteURL(cmd, ghrepo.FullName(forkedRepo))
forkedRepoCloneURL := formatRemoteURL(cmd, forkedRepo)
cloneDir, err := runClone(forkedRepoCloneURL, []string{})
if err != nil {
return fmt.Errorf("failed to clone fork: %w", err)
@ -588,7 +592,7 @@ func repoView(cmd *cobra.Command, args []string) error {
fullName := ghrepo.FullName(toView)
openURL := fmt.Sprintf("https://github.com/%s", fullName)
openURL := generateRepoURL(toView, "")
if web {
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
return utils.OpenInBrowser(openURL)

View file

@ -330,23 +330,22 @@ func determineBaseRepo(apiClient *api.Client, cmd *cobra.Command, ctx context.Co
return baseRepo, nil
}
func formatRemoteURL(cmd *cobra.Command, fullRepoName string) string {
func formatRemoteURL(cmd *cobra.Command, repo ghrepo.Interface) string {
ctx := contextForCommand(cmd)
protocol := "https"
var protocol string
cfg, err := ctx.Config()
if err != nil {
fmt.Fprintf(colorableErr(cmd), "%s failed to load config: %s. using defaults\n", utils.Yellow("!"), err)
} else {
cfgProtocol, _ := cfg.Get(defaultHostname, "git_protocol")
protocol = cfgProtocol
protocol, _ = cfg.Get(repo.RepoHost(), "git_protocol")
}
if protocol == "ssh" {
return fmt.Sprintf("git@%s:%s.git", defaultHostname, fullRepoName)
return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
return fmt.Sprintf("https://%s/%s.git", defaultHostname, fullRepoName)
return fmt.Sprintf("https://%s/%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
func determineEditor(cmd *cobra.Command) (string, error) {

View file

@ -2,6 +2,8 @@ package command
import (
"testing"
"github.com/cli/cli/internal/ghrepo"
)
func TestChangelogURL(t *testing.T) {
@ -43,7 +45,7 @@ func TestChangelogURL(t *testing.T) {
func TestRemoteURLFormatting_no_config(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
result := formatRemoteURL(repoForkCmd, "OWNER/REPO")
result := formatRemoteURL(repoForkCmd, ghrepo.New("OWNER", "REPO"))
eq(t, result, "https://github.com/OWNER/REPO.git")
}
@ -56,6 +58,6 @@ hosts:
git_protocol: ssh
`
initBlankContext(cfg, "OWNER/REPO", "master")
result := formatRemoteURL(repoForkCmd, "OWNER/REPO")
result := formatRemoteURL(repoForkCmd, ghrepo.New("OWNER", "REPO"))
eq(t, result, "git@github.com:OWNER/REPO.git")
}

View file

@ -62,8 +62,7 @@ func (c *blankContext) SetRemotes(stubs map[string]string) {
ownerWithName := strings.SplitN(repo, "/", 2)
c.remotes = append(c.remotes, &Remote{
Remote: &git.Remote{Name: remoteName},
Owner: ownerWithName[0],
Repo: ownerWithName[1],
Repo: ghrepo.New(ownerWithName[0], ownerWithName[1]),
})
}
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"sort"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/git"
@ -31,22 +32,31 @@ type Context interface {
// unusually large number of git remotes
const maxRemotesForLookup = 5
// ResolveRemotesToRepos takes in a list of git remotes and fetches more information about the repositories they map to.
// Only the git remotes belonging to the same hostname are ever looked up; all others are ignored.
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] {
var hostname string
var repos []ghrepo.Interface
for i, r := range remotes {
if i == 0 {
hostname = r.RepoHost()
} else if !strings.EqualFold(r.RepoHost(), hostname) {
// ignore all remotes for a hostname different to that of the 1st remote
continue
}
repos = append(repos, r)
if ghrepo.IsSame(r, baseOverride) {
foundBaseOverride = true
}
if len(repos) == maxRemotesForLookup {
break
}
}
if hasBaseOverride && !foundBaseOverride {
// additionally, look up the explicitly specified base repo if it's not

View file

@ -57,18 +57,22 @@ func (r Remotes) Less(i, j int) bool {
// Remote represents a git remote mapped to a GitHub repository
type Remote struct {
*git.Remote
Owner string
Repo string
Repo ghrepo.Interface
}
// RepoName is the name of the GitHub repository
func (r Remote) RepoName() string {
return r.Repo
return r.Repo.RepoName()
}
// RepoOwner is the name of the GitHub account that owns the repo
func (r Remote) RepoOwner() string {
return r.Owner
return r.Repo.RepoOwner()
}
// RepoHost is the GitHub hostname that the remote points to
func (r Remote) RepoHost() string {
return r.Repo.RepoHost()
}
// TODO: accept an interface instead of git.RemoteSet
@ -86,8 +90,7 @@ func translateRemotes(gitRemotes git.RemoteSet, urlTranslate func(*url.URL) *url
}
remotes = append(remotes, &Remote{
Remote: r,
Owner: repo.RepoOwner(),
Repo: repo.RepoName(),
Repo: repo,
})
}
return

View file

@ -22,9 +22,9 @@ func eq(t *testing.T, got interface{}, expected interface{}) {
func Test_Remotes_FindByName(t *testing.T) {
list := Remotes{
&Remote{Remote: &git.Remote{Name: "mona"}, Owner: "monalisa", Repo: "myfork"},
&Remote{Remote: &git.Remote{Name: "origin"}, Owner: "monalisa", Repo: "octo-cat"},
&Remote{Remote: &git.Remote{Name: "upstream"}, Owner: "hubot", Repo: "tools"},
&Remote{Remote: &git.Remote{Name: "mona"}, Repo: ghrepo.New("monalisa", "myfork")},
&Remote{Remote: &git.Remote{Name: "origin"}, Repo: ghrepo.New("monalisa", "octo-cat")},
&Remote{Remote: &git.Remote{Name: "upstream"}, Repo: ghrepo.New("hubot", "tools")},
}
r, err := list.FindByName("upstream", "origin")
@ -84,13 +84,11 @@ func Test_resolvedRemotes_triangularSetup(t *testing.T) {
Remotes: Remotes{
&Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "OWNER",
Repo: "REPO",
Repo: ghrepo.New("OWNER", "REPO"),
},
&Remote{
Remote: &git.Remote{Name: "fork"},
Owner: "MYSELF",
Repo: "REPO",
Repo: ghrepo.New("MYSELF", "REPO"),
},
},
Network: api.RepoNetworkResult{
@ -157,8 +155,7 @@ func Test_resolvedRemotes_forkLookup(t *testing.T) {
Remotes: Remotes{
&Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "OWNER",
Repo: "REPO",
Repo: ghrepo.New("OWNER", "REPO"),
},
},
Network: api.RepoNetworkResult{
@ -190,8 +187,7 @@ func Test_resolvedRemotes_clonedFork(t *testing.T) {
Remotes: Remotes{
&Remote{
Remote: &git.Remote{Name: "origin"},
Owner: "OWNER",
Repo: "REPO",
Repo: ghrepo.New("OWNER", "REPO"),
},
},
Network: api.RepoNetworkResult{

View file

@ -6,13 +6,13 @@ import (
"strings"
)
// TODO these are sprinkled across command, context, config, and ghrepo
const defaultHostname = "github.com"
// 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
@ -23,6 +23,15 @@ func New(owner, repo string) Interface {
}
}
// NewWithHost is like New with an explicit host name
func NewWithHost(owner, repo, hostname string) Interface {
return &ghRepo{
owner: owner,
name: repo,
hostname: 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())
@ -39,32 +48,52 @@ func FromFullName(nwo string) (Interface, error) {
return &r, nil
}
// FromURL extracts the GitHub repository information from a URL
// FromURL extracts the GitHub repository information from a git remote URL
func FromURL(u *url.URL) (Interface, error) {
if !strings.EqualFold(u.Hostname(), defaultHostname) && !strings.EqualFold(u.Hostname(), "www."+defaultHostname) {
return nil, fmt.Errorf("unsupported hostname: %s", u.Hostname())
if u.Hostname() == "" {
return nil, fmt.Errorf("no hostname detected")
}
parts := strings.SplitN(strings.TrimPrefix(u.Path, "/"), "/", 3)
if len(parts) < 2 {
parts := strings.SplitN(strings.Trim(u.Path, "/"), "/", 3)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid path: %s", u.Path)
}
return New(parts[0], strings.TrimSuffix(parts[1], ".git")), nil
return &ghRepo{
owner: parts[0],
name: strings.TrimSuffix(parts[1], ".git"),
hostname: normalizeHostname(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())
strings.EqualFold(a.RepoName(), b.RepoName()) &&
normalizeHostname(a.RepoHost()) == normalizeHostname(b.RepoHost())
}
type ghRepo struct {
owner string
name string
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 {
if r.hostname != "" {
return r.hostname
}
return defaultHostname
}

View file

@ -12,31 +12,78 @@ func Test_repoFromURL(t *testing.T) {
name string
input string
result string
host string
err error
}{
{
name: "github.com URL",
input: "https://github.com/monalisa/octo-cat.git",
result: "monalisa/octo-cat",
host: "github.com",
err: nil,
},
{
name: "github.com URL with trailing slash",
input: "https://github.com/monalisa/octo-cat/",
result: "monalisa/octo-cat",
host: "github.com",
err: nil,
},
{
name: "www.github.com URL",
input: "http://www.GITHUB.com/monalisa/octo-cat.git",
result: "monalisa/octo-cat",
host: "github.com",
err: nil,
},
{
name: "unsupported hostname",
input: "https://example.com/one/two",
name: "too many path components",
input: "https://github.com/monalisa/octo-cat/pulls",
result: "",
err: errors.New("unsupported hostname: example.com"),
host: "",
err: errors.New("invalid path: /monalisa/octo-cat/pulls"),
},
{
name: "non-GitHub hostname",
input: "https://example.com/one/two",
result: "one/two",
host: "example.com",
err: nil,
},
{
name: "filesystem path",
input: "/path/to/file",
result: "",
err: errors.New("unsupported hostname: "),
host: "",
err: errors.New("no hostname detected"),
},
{
name: "filesystem path with scheme",
input: "file:///path/to/file",
result: "",
host: "",
err: errors.New("no hostname detected"),
},
{
name: "github.com SSH URL",
input: "ssh://github.com/monalisa/octo-cat.git",
result: "monalisa/octo-cat",
host: "github.com",
err: nil,
},
{
name: "github.com HTTPS+SSH URL",
input: "https+ssh://github.com/monalisa/octo-cat.git",
result: "monalisa/octo-cat",
host: "github.com",
err: nil,
},
{
name: "github.com git URL",
input: "git://github.com/monalisa/octo-cat.git",
result: "monalisa/octo-cat",
host: "github.com",
err: nil,
},
}
@ -61,6 +108,9 @@ func Test_repoFromURL(t *testing.T) {
if tt.result != got {
t.Errorf("expected %q, got %q", tt.result, got)
}
if tt.host != repo.RepoHost() {
t.Errorf("expected %q, got %q", tt.host, repo.RepoHost())
}
})
}
}