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
38 changes: 36 additions & 2 deletions cmd/git-remote-entire/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"time"

"github.com/entireio/cli/cmd/entire/cli/auth"
"github.com/entireio/cli/cmd/entire/cli/gitremote"
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
"github.com/entireio/cli/internal/entireclient/clusterdiscovery"
"github.com/entireio/cli/internal/entireclient/httpclient"
Expand Down Expand Up @@ -89,8 +90,10 @@ func run(args []string) int {
case parsedURL.Scheme != "entire":
fmt.Fprintf(os.Stderr, "fatal: unsupported URL scheme %q (expected 'entire')\n", parsedURL.Scheme)
return 128
case parsedURL.Host == "":
fmt.Fprintf(os.Stderr, "fatal: missing host in URL %q\n", rawURL)
case parsedURL.Host == "" || gitremote.IsSupportedForge(parsedURL.Host):
// Cluster host absent (empty, or a forge id in its slot);
// missingClusterHostMessage renders the actionable hint.
fmt.Fprint(os.Stderr, missingClusterHostMessage(parsedURL, rawURL))
return 128
}

Expand Down Expand Up @@ -210,6 +213,37 @@ func fatalMessage(err error, parsedURL *url.URL) string {
return fmt.Sprintf("fatal: %v\n", err)
}

// missingClusterHostMessage renders the stderr "fatal: …" line for an entire://
// URL that omits its cluster host. Two shapes reach here: a forge id typed
// where the host belongs (entire://gh/owner/repo, Host="gh") and an empty host
// (entire:///gh/owner/repo, Host=""). When the reconstructed shorthand is a
// complete forge/owner/repo triple that `entire repo clone` can resolve, it
// points at the interactive picker; a partial path (entire://gh,
// entire://gh/owner) or a non-forge segment falls back to the plain
// missing-host error rather than suggesting a clone command that would reject
// the ref. Kept pure so it's unit-testable.
func missingClusterHostMessage(parsedURL *url.URL, rawURL string) string {
// Reconstruct the forge/owner/repo shorthand the user likely intended: a
// forge id in the host slot sits in front of the path; an empty host
// already has it there.
shorthand := strings.TrimPrefix(parsedURL.Path, "/")
if parsedURL.Host != "" {
shorthand = parsedURL.Host + "/" + shorthand
}
// Only point at `entire repo clone` for a complete forge/owner/repo triple
// (the shape parseMirrorCloneRef accepts); anything shorter would relocate
// the failure into a clone command that rejects the ref.
seg := strings.Split(strings.Trim(shorthand, "/"), "/")
if len(seg) != 3 || seg[0] == "" || seg[1] == "" || seg[2] == "" || !gitremote.IsSupportedForge(seg[0]) {
return fmt.Sprintf("fatal: missing host in URL %q\n", rawURL)
}
return fmt.Sprintf(
"fatal: entire:// URL is missing its cluster host (%q is a forge id, not a host).\n"+
"The full form is entire://<cluster-host>/%s/<owner>/<repo>.\n"+
"To pick a mirror interactively, run:\n\n entire repo clone /%s\n",
seg[0], seg[0], strings.Join(seg, "/"))
}

// loadedVersion populates the build info and returns the resolved version.
func loadedVersion() string {
versioninfo.Load()
Expand Down
92 changes: 92 additions & 0 deletions cmd/git-remote-entire/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,98 @@ func TestFatalMessage(t *testing.T) {
}
}

func TestMissingClusterHostMessage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rawURL string
contains []string
notContains []string
}{
{
// The motivating case: forge id typed where the cluster host belongs.
name: "forge id in host slot points at repo clone",
rawURL: "entire://gh/entire.io/cli",
contains: []string{"missing its cluster host", `"gh" is a forge id`, "entire repo clone /gh/entire.io/cli"},
},
{
// Empty host but the path already reads as a forge shorthand.
name: "empty host with forge path points at repo clone",
rawURL: "entire:///gh/entire.io/cli",
contains: []string{"missing its cluster host", "entire repo clone /gh/entire.io/cli"},
},
Comment thread
Soph marked this conversation as resolved.
{
// Empty host, leading segment is not a known forge → generic error.
name: "empty host with non-forge path falls back",
rawURL: "entire:///not-a-forge/owner/repo",
contains: []string{`fatal: missing host in URL "entire:///not-a-forge/owner/repo"`},
notContains: []string{"entire repo clone"},
},
{
name: "bare scheme falls back",
rawURL: "entire://",
contains: []string{`fatal: missing host in URL "entire://"`},
notContains: []string{"entire repo clone"},
},
{
// Not enough path to form owner/repo → not worth pointing at clone.
name: "empty host single-segment path falls back",
rawURL: "entire:///gh",
contains: []string{`fatal: missing host in URL "entire:///gh"`},
notContains: []string{"entire repo clone"},
},
{
// Forge in host slot but no owner/repo — the shorthand `/gh` would be
// rejected by `entire repo clone`, so fall back rather than suggest it.
name: "forge in host slot without path falls back",
rawURL: "entire://gh",
contains: []string{`fatal: missing host in URL "entire://gh"`},
notContains: []string{"entire repo clone"},
},
{
// Forge in host slot with owner but no repo — incomplete triple.
name: "forge in host slot with owner only falls back",
rawURL: "entire://gh/owner",
contains: []string{`fatal: missing host in URL "entire://gh/owner"`},
notContains: []string{"entire repo clone"},
},
{
// Empty host, forge + owner but no repo — incomplete triple.
name: "empty host forge and owner only falls back",
rawURL: "entire:///gh/owner",
contains: []string{`fatal: missing host in URL "entire:///gh/owner"`},
notContains: []string{"entire repo clone"},
},
{
// Too many segments — not the gh/<owner>/<repo> shape either.
name: "forge in host slot with extra path segment falls back",
rawURL: "entire://gh/owner/repo/extra",
contains: []string{`fatal: missing host in URL "entire://gh/owner/repo/extra"`},
notContains: []string{"entire repo clone"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
parsed, err := url.Parse(tc.rawURL)
if err != nil {
t.Fatalf("parse %q: %v", tc.rawURL, err)
}
got := missingClusterHostMessage(parsed, tc.rawURL)
for _, sub := range tc.contains {
if !strings.Contains(got, sub) {
t.Errorf("missingClusterHostMessage(%q) = %q, missing %q", tc.rawURL, got, sub)
}
}
for _, sub := range tc.notContains {
if strings.Contains(got, sub) {
t.Errorf("missingClusterHostMessage(%q) = %q, should not contain %q", tc.rawURL, got, sub)
}
}
})
}
}

func TestCoreTrusted(t *testing.T) {
t.Parallel()
trusted := []string{"https://core.us.entire.io", "https://core.eu.entire.io/"}
Expand Down
Loading