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
6 changes: 6 additions & 0 deletions internal/config/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
Expand Down
6 changes: 6 additions & 0 deletions internal/config/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 38 additions & 4 deletions pkg/cmd/repo/shared/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,31 @@ func (r RepoRef) FullName() string {
return r.Owner + "/" + r.Name
}

// ParseRepoArg accepts the canonical `<owner>/<name>` 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 `<owner>/<name>` 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:
Expand All @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions pkg/cmd/repo/shared/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading