fix(trail): reject host-like --repo forge in the bare path form · Entire
fix(trail): reject host-like --repo forge in the bare path form
4a109f1·
toothbrush·3w ago·4 files·+22 added/-4 removed
The bare forge/owner/repo form accepted parts[0] as the forge without
checking it is a known short forge id, so --repo github.com/acme/app
(or gitlab.com/...) slipped through and produced a malformed
/api/v1/trails/
Validate the forge segment against gitremote.IsSupportedForge and fail with a clear message pointing at a forge id ("gh") or a clone URL.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
fb56d44a31fdView transcript
?\Investigate CLI Repo and Branch FlagsClaude Code·Opus 4.8[1m]·2 steps
Changes
4
cmd/entire/cli
agentimport
Magentimport.go+1/-1
gitremote
Mgitremote.go+9
Mtrail_cmd.go+9/-3
Mtrail_repo_flag_test.go+3
69 unmodified lines
70
71
72
73
73
74
75
76
69 unmodified lines
// Get returns the importer registered under name.
//
//nolint:ireturn // Importer is the intended polymorphic seam returned to callers.
func Get(name string) (Importer, bool) {
for _, imp := range importers {
if imp.Name() == name {
Mcmd/entire/cli/agentimport/agentimport.go+1/-1
55 unmodified lines
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
55 unmodified lines
return m
}()
// IsSupportedForge reports whether forge is a known short forge id (e.g. "gh")
// understood by the trails API. It rejects forge hostnames ("github.com") and
// any other unrecognized value, so callers parsing a bare forge/owner/repo
// triple can fail clearly instead of forwarding a malformed forge to the API.
func IsSupportedForge(forge string) bool {
_, ok := forgeToHost[forge]
return ok
}
// CanonicalHost returns the canonical public host of the upstream forge.
//
// For direct git URLs this is just Host. For entire:// remotes — whose Host is
Mcmd/entire/cli/gitremote/gitremote.go+9
1721 unmodified lines
1722
1723
1724
1725
1726
1725
1726
1727
1728
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1721 unmodified lines
// fall through to the URL parser, which understands hosts and schemes.
if !strings.Contains(raw, "://") && !strings.Contains(raw, "@"){
parts := strings.Split(strings.Trim(raw, "/"), "/")
if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" {
return parts[0], parts[1], strings.TrimSuffix(parts[2], ".git"), nil
}
if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" {
return "", "", "", fmt.Errorf("invalid --repo %q: expected forge/owner/repo (e.g. gh/acme/app) or a clone URL", raw)
}
return "", "", "", fmt.Errorf("invalid --repo %q: expected forge/owner/repo (e.g. gh/acme/app) or a clone URL", raw)
// parts[0] must be a short forge id ("gh"), not a hostname. A host-like
// value (github.com/acme/app) would otherwise be forwarded verbatim and
// the server would reject the malformed path with an opaque error.
if !gitremote.IsSupportedForge(parts[0]) {
return "", "", "", fmt.Errorf("invalid --repo %q: %q is not a supported forge id (use a forge id like \"gh\", or pass a clone URL such as https://github.com/%s/%s)", raw, parts[0], parts[1], parts[2])
}
return parts[0], parts[1], strings.TrimSuffix(parts[2], ".git"), nil
}
info, perr := gitremote.ParseURL(raw)
if perr != nil {
Mcmd/entire/cli/trail_cmd.go+9/-3
28 unmodified lines
29
30
31
32
33
34
35
36
37
28 unmodified lines
{name: "forge plus owner only", raw: "gh/acme", wantErr: true},
{name: "four segments", raw: "gh/acme/app/extra", wantErr: true},
{name: "unsupported forge host", raw: "git@gitlab.com:acme/app.git", wantErr: true},
{name: "bare host instead of forge id", raw: "github.com/acme/app", wantErr: true},
{name: "bare unsupported forge host", raw: "gitlab.com/acme/app", wantErr: true},
{name: "unknown short forge id", raw: "zz/acme/app", wantErr: true},
}
for _, tt := range tests {