Skip to content

Commit 437da3b

Browse files
fix(code-hosting): recognize SSH repository hosts with userinfo
Use parsed hostnames instead of raw netloc values when matching configured code-hosting domains. This keeps ssh://git@host and explicit-port repository URLs on the supported code-hosting path while preserving git clone handling for SSH repository sources. Add focused regression coverage for SSH URL userinfo and explicit-port repository URL helpers.
1 parent f9867be commit 437da3b

3 files changed

Lines changed: 88 additions & 11 deletions

File tree

openviking/parse/parsers/code/code.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ async def parse(self, source: Union[str, Path], instruction: str = "", **kwargs)
121121
)
122122
elif source_str.startswith(("http://", "https://", "git://", "ssh://")):
123123
repo_url, branch, commit = self._parse_repo_source(source_str, **kwargs)
124-
if self._is_github_url(repo_url):
124+
if repo_url.startswith(("http://", "https://", "git://")) and self._is_github_url(
125+
repo_url
126+
):
125127
# Use GitHub ZIP API: supports branch names, tags, and commit SHAs
126128
local_dir, repo_name = await self._github_zip_download(
127129
repo_url, branch or commit, temp_local_dir
@@ -265,10 +267,18 @@ def _normalize_repo_url(self, url: str) -> str:
265267
base_parts = path_parts[: git_index + 1]
266268

267269
config = get_openviking_config()
268-
if (
269-
parsed.netloc in config.code.github_domains + config.code.gitlab_domains
270-
and len(path_parts) >= 2
271-
):
270+
domains = {
271+
domain.lower() for domain in config.code.github_domains + config.code.gitlab_domains
272+
}
273+
host = (parsed.hostname or "").lower()
274+
host_candidates = {host} if host else set()
275+
try:
276+
port = parsed.port
277+
except ValueError:
278+
port = None
279+
if host and port is not None:
280+
host_candidates.add(f"{host}:{port}")
281+
if host_candidates & domains and len(path_parts) >= 2:
272282
base_parts = path_parts[:2]
273283
base_path = "/" + "/".join(base_parts)
274284
return parsed._replace(path=base_path, query="", fragment="").geturl()

openviking/utils/code_hosting_utils.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,37 @@
88
"""
99

1010
from typing import Optional
11-
from urllib.parse import urlparse
11+
from urllib.parse import ParseResult, urlparse
1212

1313
from openviking_cli.utils.config import get_openviking_config
1414

1515

16+
def _domain_matches(parsed: ParseResult, domains: list[str]) -> bool:
17+
"""Return True when parsed URL host matches configured domains.
18+
19+
``urlparse().netloc`` includes optional userinfo and port values. Repository
20+
clone URLs commonly use forms like ``ssh://git@github.com/org/repo.git``,
21+
where the netloc is ``git@github.com`` but the actual host is
22+
``github.com``.
23+
"""
24+
hostname = parsed.hostname
25+
if not hostname:
26+
return False
27+
28+
normalized_domains = {domain.lower() for domain in domains}
29+
host = hostname.lower()
30+
candidates = {host}
31+
32+
try:
33+
port = parsed.port
34+
except ValueError:
35+
port = None
36+
if port is not None:
37+
candidates.add(f"{host}:{port}")
38+
39+
return any(candidate in normalized_domains for candidate in candidates)
40+
41+
1642
def parse_code_hosting_url(url: str) -> Optional[str]:
1743
"""Parse code hosting platform URL to get org/repo path.
1844
@@ -60,7 +86,7 @@ def parse_code_hosting_url(url: str) -> Optional[str]:
6086

6187
# For GitHub/GitLab URLs with org/repo structure
6288
if (
63-
parsed.netloc in config.code.github_domains + config.code.gitlab_domains
89+
_domain_matches(parsed, config.code.github_domains + config.code.gitlab_domains)
6490
and len(path_parts) >= 2
6591
):
6692
# Take first two parts: org/repo
@@ -86,7 +112,7 @@ def is_github_url(url: str) -> bool:
86112
True if the URL is a GitHub URL
87113
"""
88114
config = get_openviking_config()
89-
return urlparse(url).netloc in config.code.github_domains
115+
return _domain_matches(urlparse(url), config.code.github_domains)
90116

91117

92118
def is_gitlab_url(url: str) -> bool:
@@ -99,7 +125,7 @@ def is_gitlab_url(url: str) -> bool:
99125
True if the URL is a GitLab URL
100126
"""
101127
config = get_openviking_config()
102-
return urlparse(url).netloc in config.code.gitlab_domains
128+
return _domain_matches(urlparse(url), config.code.gitlab_domains)
103129

104130

105131
def is_code_hosting_url(url: str) -> bool:
@@ -127,7 +153,7 @@ def is_code_hosting_url(url: str) -> bool:
127153
host_part = url[4:].split(":", 1)[0]
128154
return host_part in all_domains
129155

130-
return urlparse(url).netloc in all_domains
156+
return _domain_matches(urlparse(url), all_domains)
131157

132158

133159
def validate_git_ssh_uri(url: str) -> None:
@@ -173,7 +199,7 @@ def is_git_repo_url(url: str) -> bool:
173199
)
174200
)
175201
parsed = urlparse(url)
176-
if parsed.netloc not in all_domains:
202+
if not _domain_matches(parsed, all_domains):
177203
return False
178204
path_parts = [p for p in parsed.path.split("/") if p]
179205
# Strip .git suffix from last part for counting

tests/test_code_hosting_utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def _mock_config():
4343
_spec.loader.exec_module(_module)
4444

4545
parse_code_hosting_url = _module.parse_code_hosting_url
46+
is_github_url = _module.is_github_url
47+
is_gitlab_url = _module.is_gitlab_url
4648
is_code_hosting_url = _module.is_code_hosting_url
4749
is_git_repo_url = _module.is_git_repo_url
4850
validate_git_ssh_uri = _module.validate_git_ssh_uri
@@ -81,6 +83,18 @@ def test_parse_code_hosting_url_https_dotgit():
8183
assert parse_code_hosting_url("https://github.com/org/repo.git") == "org/repo"
8284

8385

86+
def test_parse_code_hosting_url_ssh_url_with_userinfo():
87+
assert parse_code_hosting_url("ssh://git@github.com/org/repo.git") == "org/repo"
88+
89+
90+
def test_parse_code_hosting_url_gitlab_ssh_url_with_userinfo():
91+
assert parse_code_hosting_url("ssh://git@gitlab.com/group/repo.git") == "group/repo"
92+
93+
94+
def test_parse_code_hosting_url_https_with_port():
95+
assert parse_code_hosting_url("https://github.com:443/org/repo") == "org/repo"
96+
97+
8498
# --- validate_git_ssh_uri ---
8599

86100

@@ -118,6 +132,25 @@ def test_is_code_hosting_url_https():
118132
assert is_code_hosting_url("https://github.com/org/repo") is True
119133

120134

135+
def test_is_code_hosting_url_ssh_url_with_userinfo():
136+
assert is_code_hosting_url("ssh://git@github.com/org/repo.git") is True
137+
138+
139+
def test_is_code_hosting_url_https_with_port():
140+
assert is_code_hosting_url("https://github.com:443/org/repo") is True
141+
142+
143+
# --- is_github_url / is_gitlab_url ---
144+
145+
146+
def test_is_github_url_ssh_url_with_userinfo():
147+
assert is_github_url("ssh://git@github.com/org/repo.git") is True
148+
149+
150+
def test_is_gitlab_url_ssh_url_with_userinfo():
151+
assert is_gitlab_url("ssh://git@gitlab.com/group/repo.git") is True
152+
153+
121154
# --- is_git_repo_url ---
122155

123156

@@ -129,6 +162,14 @@ def test_is_git_repo_url_https_repo():
129162
assert is_git_repo_url("https://github.com/org/repo") is True
130163

131164

165+
def test_is_git_repo_url_ssh_url_with_userinfo():
166+
assert is_git_repo_url("ssh://git@github.com/org/repo.git") is True
167+
168+
169+
def test_is_git_repo_url_https_with_port():
170+
assert is_git_repo_url("https://github.com:443/org/repo") is True
171+
172+
132173
def test_is_git_repo_url_https_issues():
133174
assert is_git_repo_url("https://github.com/org/repo/issues/123") is False
134175

0 commit comments

Comments
 (0)