git-remote-entire: only suggest clone for a complete forge/owner/repo ref · Entire

git-remote-entire: only suggest clone for a complete forge/owner/repo ref

6050a60→main·

Soph·1w ago·2 files·+49 added/-21 removed

PR review (Cursor Bugbot + Copilot) flagged that missingClusterHostMessage unconditionally pointed at entire repo clone whenever a forge id sat in the host slot, even without owner/repo. So entire://gh suggested entire repo clone /gh and entire://gh/owner suggested /gh/owner — refs that parseMirrorCloneRef (^/?gh//$) rejects immediately. The empty-host branch already fell back correctly, so the two equivalent mistakes were handled inconsistently.

Reconstruct the intended shorthand once (forge id in host slot vs. empty host) and only emit the hint when it is a complete forge/owner/repo triple; anything shorter or longer falls back to the plain missing-host error. Collapsing the two branches also lets clusterHostHint be inlined.

Add regression cases for entire://gh, entire://gh/owner, entire:///gh/owner, and a too-many-segments path.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Sessions

01KWYNH3035PPHFTMD9J6GWEYWView transcript

Changes

2

215 unmodified lines

// 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 intended repo shorthand is a
// recoverable — the host slot, or the path's leading segment, is a known forge
// id — it points at the interactive picker (`entire repo clone <shorthand>`),
// which resolves the mirror and clones a fully-qualified URL. Anything else
// (bare entire://, a non-forge leading segment) falls back to the plain
// missing-host error. Kept pure so it's unit-testable.
// (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 {
    // Forge id in the host slot: the shorthand is /<host><path>.
    if gitremote.IsSupportedForge(parsedURL.Host) {
        return clusterHostHint(parsedURL.Host, "/"+parsedURL.Host+parsedURL.Path)
    }
    // 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
    }
    // Empty host with a forge-led path: the path is already the shorthand.
    if forge, _, ok := strings.Cut(strings.TrimPrefix(parsedURL.Path, "/"), "/"); ok && gitremote.IsSupportedForge(forge) {
        return clusterHostHint(forge, parsedURL.Path)
    }
    // 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: missing host in URL %q\n", rawURL)
}

// clusterHostHint is the actionable message pointing a host-less entire:// URL
// at the `entire repo clone` picker, which resolves the mirror for the given
// forge shorthand.
func clusterHostHint(forge, shorthand string) string {
    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",
        forge, forge, shorthand)
}

// loadedVersion populates the build info and returns the resolved version.

Mcmd/git-remote-entire/main.go+20/-21

212 unmodified lines

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) {