From af9887217a756bdcfbcc13e040e3bbc881829791 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 07:25:34 +0000 Subject: [PATCH] fix(setup): broaden GitHub remote parsing Co-Authored-By: Virgil --- cmd/setup/cmd_repo.go | 46 +++++++++++++++++++++++++++++--------- cmd/setup/cmd_repo_test.go | 22 ++++++++++++++++++ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/cmd/setup/cmd_repo.go b/cmd/setup/cmd_repo.go index f4b5a35..b0ebd2a 100644 --- a/cmd/setup/cmd_repo.go +++ b/cmd/setup/cmd_repo.go @@ -8,6 +8,7 @@ package setup import ( "fmt" + "net/url" "os" "os/exec" "path/filepath" @@ -294,23 +295,46 @@ func detectGitHubRepo() string { return "" } - url := strings.TrimSpace(string(output)) + return parseGitHubRepoURL(strings.TrimSpace(string(output))) +} - // Handle SSH format: git@github.com:owner/repo.git - if strings.HasPrefix(url, "git@github.com:") { - repo := strings.TrimPrefix(url, "git@github.com:") - repo = strings.TrimSuffix(repo, ".git") - return repo +// parseGitHubRepoURL extracts owner/repo from a GitHub remote URL. +// +// Supports the common remote formats used by git: +// - git@github.com:owner/repo.git +// - ssh://git@github.com/owner/repo.git +// - https://github.com/owner/repo.git +// - git://github.com/owner/repo.git +func parseGitHubRepoURL(remote string) string { + remote = strings.TrimSpace(remote) + if remote == "" { + return "" } - // Handle HTTPS format: https://github.com/owner/repo.git - if strings.Contains(url, "github.com/") { - parts := strings.Split(url, "github.com/") - if len(parts) == 2 { - repo := strings.TrimSuffix(parts[1], ".git") + // Handle SSH-style scp syntax first. + if strings.HasPrefix(remote, "git@github.com:") { + repo := strings.TrimPrefix(remote, "git@github.com:") + return strings.TrimSuffix(repo, ".git") + } + + if parsed, err := url.Parse(remote); err == nil && parsed.Host != "" { + host := strings.TrimPrefix(parsed.Hostname(), "www.") + if host == "github.com" { + repo := strings.TrimPrefix(parsed.Path, "/") + repo = strings.TrimSuffix(repo, ".git") + repo = strings.TrimSuffix(repo, "/") return repo } } + if strings.Contains(remote, "github.com/") { + parts := strings.SplitN(remote, "github.com/", 2) + if len(parts) == 2 { + repo := strings.TrimPrefix(parts[1], "/") + repo = strings.TrimSuffix(repo, ".git") + return strings.TrimSuffix(repo, "/") + } + } + return "" } diff --git a/cmd/setup/cmd_repo_test.go b/cmd/setup/cmd_repo_test.go index ab2a493..3d9ddf0 100644 --- a/cmd/setup/cmd_repo_test.go +++ b/cmd/setup/cmd_repo_test.go @@ -28,3 +28,25 @@ func TestDetectProjectType_PrefersPackageOverComposer(t *testing.T) { require.Equal(t, "node", detectProjectType(dir)) } + +func TestParseGitHubRepoURL_Good(t *testing.T) { + cases := map[string]string{ + "git@github.com:owner/repo.git": "owner/repo", + "ssh://git@github.com/owner/repo.git": "owner/repo", + "https://github.com/owner/repo.git": "owner/repo", + "git://github.com/owner/repo.git": "owner/repo", + "https://www.github.com/owner/repo": "owner/repo", + "git@github.com:owner/nested/repo.git": "owner/nested/repo", + "ssh://git@github.com/owner/nested/repo/": "owner/nested/repo", + "ssh://git@github.com:443/owner/repo.git": "owner/repo", + "https://example.com/owner/repo.git": "", + "git@bitbucket.org:owner/repo.git": "", + " ssh://git@github.com/owner/repo.git ": "owner/repo", + } + + for remote, expected := range cases { + t.Run(remote, func(t *testing.T) { + require.Equal(t, expected, parseGitHubRepoURL(remote)) + }) + } +}