diff --git a/internal/config/hosts.go b/internal/config/hosts.go index 395a072..01ab3d6 100644 --- a/internal/config/hosts.go +++ b/internal/config/hosts.go @@ -239,6 +239,12 @@ func NormalizeHost(host string) string { if h == "" { return "" } + // C24: drop the default HTTPS port (`shithub.sh:443` → `shithub.sh`) + // so users pasting `Host:` headers from HTTP traces get a hosts.yml + // match instead of "no token configured for host". We intentionally + // do NOT strip `:80` or other ports — those are meaningful + // non-defaults that users explicitly opted into. + h = strings.TrimSuffix(h, ":443") if !isValidHost(h) { return "" } diff --git a/internal/config/hosts_test.go b/internal/config/hosts_test.go index fa50c11..c34de5a 100644 --- a/internal/config/hosts_test.go +++ b/internal/config/hosts_test.go @@ -279,6 +279,12 @@ func TestNormalizeHost(t *testing.T) { "shithub.sh:8443": "shithub.sh:8443", "[::1]": "[::1]", "[::1]:8443": "[::1]:8443", + // C-audit C24: explicit `:443` (HTTPS default) is normalized + // off so users pasting Host headers from HTTP traces hit the + // hosts.yml entry. Non-default ports must survive. + "shithub.sh:443": "shithub.sh", + "https://shithub.sh:443": "shithub.sh", + "SHITHUB.SH:443": "shithub.sh", } for in, want := range cases { if got := NormalizeHost(in); got != want { diff --git a/pkg/cmd/repo/shared/resolve.go b/pkg/cmd/repo/shared/resolve.go index 0c3c9db..95ae41b 100644 --- a/pkg/cmd/repo/shared/resolve.go +++ b/pkg/cmd/repo/shared/resolve.go @@ -38,15 +38,31 @@ func (r RepoRef) FullName() string { return r.Owner + "/" + r.Name } -// ParseRepoArg accepts the canonical `/` form (and tolerates -// a host prefix `host/owner/name`). Returns an error for anything else; -// callers that want to accept URLs should pre-parse via internal/git -// before calling. +// ParseRepoArg accepts the canonical `/` form, `host/owner/name`, +// HTTPS/SSH URL forms, and tolerates a trailing `.git` suffix on any of +// them (C-audit C16+C17). gh-compat: `gh issue list -R https://github.com/cli/cli` +// works, and copy-pasting `git remote get-url origin` into `-R` works. func ParseRepoArg(s string) (RepoRef, error) { s = strings.TrimSpace(s) if s == "" { return RepoRef{}, errors.New("repo: argument is empty") } + + // URL forms: route through the existing ParseRemoteURL which handles + // https://, http://, ssh://, and the SCP-like git@host:owner/repo + // form. ParseRemoteURL strips the `.git` suffix already. + if isLikelyURL(s) { + rem, err := git.ParseRemoteURL(s) + if err != nil { + return RepoRef{}, fmt.Errorf("repo: parse %q: %w", s, err) + } + return RepoRef{Host: rem.Host, Owner: rem.Owner, Name: rem.Repo}, nil + } + + // Bare owner/name (or host/owner/name) form. Strip a trailing + // `.git` from the repo segment so scripts that paste the literal + // remote-URL tail (`mfwolffe/repo.git`) don't 404. + s = strings.TrimSuffix(s, ".git") parts := strings.Split(s, "/") switch len(parts) { case 2: @@ -64,6 +80,24 @@ func ParseRepoArg(s string) (RepoRef, error) { } } +// isLikelyURL reports whether s looks like a remote URL rather than a +// bare owner/name pair. Conservative: only the shapes ParseRemoteURL +// actually accepts trigger the URL path, so users with a literal `:` +// or `@` in an owner segment still hit the bare path with a clear +// error. +func isLikelyURL(s string) bool { + if strings.Contains(s, "://") { + return true + } + // SCP-like: user@host:path + if at := strings.IndexByte(s, '@'); at > 0 { + if colon := strings.IndexByte(s, ':'); colon > at { + return true + } + } + return false +} + // Resolver is the contract a command uses to figure out which repo it's // acting on. Production wires this from the Factory; tests inject a fake // to avoid touching the filesystem. diff --git a/pkg/cmd/repo/shared/resolve_test.go b/pkg/cmd/repo/shared/resolve_test.go index 53a89bf..829756e 100644 --- a/pkg/cmd/repo/shared/resolve_test.go +++ b/pkg/cmd/repo/shared/resolve_test.go @@ -26,6 +26,19 @@ func TestParseRepoArg(t *testing.T) { {"", false, "", "", ""}, {"a/b/c/d", false, "", "", ""}, {"/x", false, "", "", ""}, + // C-audit C17: `.git` suffix is stripped on bare forms so + // `git remote get-url origin | xargs -I{} shithub ... -R {}` + // works. + {"octo/hello.git", true, "octo", "hello", ""}, + {"shithub.sh/octo/hello.git", true, "octo", "hello", "shithub.sh"}, + // C-audit C16: HTTPS URL forms (with and without .git suffix). + {"https://shithub.sh/octo/hello", true, "octo", "hello", "shithub.sh"}, + {"https://shithub.sh/octo/hello.git", true, "octo", "hello", "shithub.sh"}, + // C-audit C16: SCP-style git@ remote URLs. + {"git@shithub.sh:octo/hello.git", true, "octo", "hello", "shithub.sh"}, + {"git@shithub.sh:octo/hello", true, "octo", "hello", "shithub.sh"}, + // Defense: malformed URL surfaces a clear error. + {"https://shithub.sh/onlyowner", false, "", "", ""}, } for _, tc := range cases { got, err := ParseRepoArg(tc.in)