diff --git a/pkg/cli/git.go b/pkg/cli/git.go index ef6b4adbce2..8bb45af9acb 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -53,9 +53,9 @@ func findGitRootForPath(path string) (string, error) { } // parseGitHubRepoSlugFromURL extracts owner/repo from a GitHub URL -// Supports HTTPS (https://github.com/owner/repo), SCP-style SSH (git@github.com:owner/repo), +// Supports HTTPS (https://github.com/owner/repo), SCP-style SSH ([user@]github.com:owner/repo), // and SSH URL scheme (ssh://git@github.com/owner/repo) formats. -// Also supports GitHub Enterprise URLs. +// Also supports GitHub Enterprise URLs and non-standard SSH usernames (e.g. example@ghe.host:owner/repo). func parseGitHubRepoSlugFromURL(url string) string { gitLog.Printf("Parsing GitHub repo slug from URL: %s", url) @@ -72,12 +72,22 @@ func parseGitHubRepoSlugFromURL(url string) string { return slug } - // Handle SCP-style SSH URLs: git@github.com:owner/repo or git@enterprise.github.com:owner/repo - sshPrefix := "git@" + githubHostWithoutScheme + ":" - if after, ok := strings.CutPrefix(url, sshPrefix); ok { - slug := after - gitLog.Printf("Extracted slug from SSH URL: %s", slug) - return slug + // Handle SCP-style SSH URLs with any username: git@github.com:owner/repo, + // or non-standard usernames like example@example.ghe.com:owner/repo (GHE), + // or username-less like github.com:owner/repo. + // Match: [user@]:/ + scpHostColon := githubHostWithoutScheme + ":" + // Try with username (user@host:path) + if _, afterAt, hasAt := strings.Cut(url, "@"); hasAt { + if after, ok := strings.CutPrefix(afterAt, scpHostColon); ok { + gitLog.Printf("Extracted slug from SCP-style SSH URL with username: %s", after) + return after + } + } + // Try without username (host:path) + if after, ok := strings.CutPrefix(url, scpHostColon); ok { + gitLog.Printf("Extracted slug from SCP-style SSH URL without username: %s", after) + return after } // Handle SSH URL scheme: ssh://git@github.com/owner/repo or ssh://github.com/owner/repo @@ -124,13 +134,6 @@ func extractHostFromRemoteURL(remoteURL string) string { } } - // SSH scp-like format: git@host:path - if after, ok := strings.CutPrefix(remoteURL, "git@"); ok { - if host, _, found := strings.Cut(after, ":"); found { - return host - } - } - // SSH URL format: ssh://git@host/path or ssh://host/path if after, ok := strings.CutPrefix(remoteURL, "ssh://"); ok { // Strip optional user info (e.g. "git@") @@ -143,6 +146,22 @@ func extractHostFromRemoteURL(remoteURL string) string { return after } + // SSH scp-like format: [user@]host:path — any username, not just git@ + // Try with username first (user@host:path) + if _, afterAt, hasAt := strings.Cut(remoteURL, "@"); hasAt { + if host, _, found := strings.Cut(afterAt, ":"); found { + return host + } + } + // Try without username (host:path) + if host, _, found := strings.Cut(remoteURL, ":"); found { + // Make sure this looks like an scp-style URL (has no slashes before the colon) + // to avoid matching "ssh://host:port/path" or "https://host:port/path" + if !strings.Contains(host, "/") { + return host + } + } + return "github.com" } diff --git a/pkg/cli/git_helpers_test.go b/pkg/cli/git_helpers_test.go index 94223709927..9b064f70460 100644 --- a/pkg/cli/git_helpers_test.go +++ b/pkg/cli/git_helpers_test.go @@ -32,6 +32,16 @@ func TestParseGitHubRepoSlugFromURL(t *testing.T) { url: "git@github.com:github/gh-aw", expected: "github/gh-aw", }, + { + name: "SSH SCP-style URL without username with .git", + url: "github.com:github/gh-aw.git", + expected: "github/gh-aw", + }, + { + name: "SSH SCP-style URL without username without .git", + url: "github.com:github/gh-aw", + expected: "github/gh-aw", + }, { name: "SSH URL scheme with .git", url: "ssh://git@github.com/github/gh-aw.git", @@ -74,6 +84,59 @@ func TestParseGitHubRepoSlugFromURL(t *testing.T) { } } +// TestParseGitHubRepoSlugFromURLGHE verifies that GHE SSH URLs with non-standard +// usernames (e.g. example@example.ghe.com:owner/repo.git) are parsed correctly +// when GITHUB_SERVER_URL is configured to point at the GHE instance. +func TestParseGitHubRepoSlugFromURLGHE(t *testing.T) { + tests := []struct { + name string + url string + expected string + }{ + { + name: "GHE SSH SCP-style with custom username and .git", + url: "example@example.ghe.com:example-org/example-repo.git", + expected: "example-org/example-repo", + }, + { + name: "GHE SSH SCP-style with custom username without .git", + url: "example@example.ghe.com:example-org/example-repo", + expected: "example-org/example-repo", + }, + { + name: "GHE SSH SCP-style with git username and .git", + url: "git@example.ghe.com:example-org/example-repo.git", + expected: "example-org/example-repo", + }, + { + name: "GHE HTTPS URL", + url: "https://example.ghe.com/example-org/example-repo.git", + expected: "example-org/example-repo", + }, + { + name: "GHE SSH URL scheme", + url: "ssh://example@example.ghe.com/example-org/example-repo.git", + expected: "example-org/example-repo", + }, + { + name: "SSH SCP-style URL for a different host should not match", + url: "example@other.host.com:example-org/example-repo.git", + expected: "", + }, + } + + t.Setenv("GITHUB_SERVER_URL", "https://example.ghe.com") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseGitHubRepoSlugFromURL(tt.url) + if result != tt.expected { + t.Errorf("parseGitHubRepoSlugFromURL(%q) = %q, expected %q", tt.url, result, tt.expected) + } + }) + } +} + func TestGetRepositorySlugFromRemote(t *testing.T) { // This test verifies that the function can execute without errors in a git repo // The actual value will depend on the repository being tested diff --git a/pkg/cli/git_test.go b/pkg/cli/git_test.go index b0f68cfb0c1..2dd2b62e53d 100644 --- a/pkg/cli/git_test.go +++ b/pkg/cli/git_test.go @@ -390,6 +390,16 @@ func TestExtractHostFromRemoteURL(t *testing.T) { url: "git@ghes.example.com:org/repo.git", expected: "ghes.example.com", }, + { + name: "GHE SSH scp-like with custom username", + url: "example@example.ghe.com:example-org/example-repo.git", + expected: "example.ghe.com", + }, + { + name: "SSH scp-like without username", + url: "github.com:owner/repo.git", + expected: "github.com", + }, { name: "GHES HTTPS without .git suffix", url: "https://ghes.example.com/org/repo",