Skip to content

Commit 9f4008f

Browse files
authored
Merge pull request #2058 from timbrel/fix-2057
2 parents 0280baf + 200a987 commit 9f4008f

File tree

3 files changed

+112
-11
lines changed

3 files changed

+112
-11
lines changed

github/github.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
from __future__ import annotations
66
import os
77
import re
8-
from webbrowser import open as open_in_browser
9-
from functools import partial
8+
import subprocess
9+
from functools import lru_cache, partial
1010
from typing import NamedTuple
11+
from webbrowser import open as open_in_browser
1112

1213
from ..common import interwebs
1314
from ..core.exceptions import FailedGithubRequest
1415
from ..core.settings import GitSavvySettings
16+
from ..core.utils import STARTUPINFO
1517

1618

1719
GITHUB_PER_PAGE_MAX = 100
@@ -33,14 +35,15 @@ class GitHubRepo(NamedTuple):
3335
token: str | None
3436

3537

38+
@lru_cache(maxsize=128)
3639
def remote_to_url(remote_url: str) -> str:
3740
"""
3841
Parse out a Github HTTP URL from a remote URI:
3942
4043
r1 = remote_to_url("git://github.com/timbrel/GitSavvy.git")
4144
assert r1 == "https://github.com/timbrel/GitSavvy"
4245
43-
r2 = remote_to_url("git@github.com:divmain/GitSavvy.git")
46+
r2 = remote_to_url("git@github.com:timbrel/GitSavvy.git")
4447
assert r2 == "https://github.com/timbrel/GitSavvy"
4548
4649
r3 = remote_to_url("https://github.com/timbrel/GitSavvy.git")
@@ -50,14 +53,14 @@ def remote_to_url(remote_url: str) -> str:
5053
if remote_url.endswith(".git"):
5154
remote_url = remote_url[:-4]
5255

53-
if remote_url.startswith("git@"):
54-
return remote_url.replace(":", "/").replace("git@", "https://")
55-
elif remote_url.startswith("git://"):
56+
if remote_url.startswith("git://"):
5657
return remote_url.replace("git://", "https://")
57-
elif remote_url.startswith("http"):
58+
if remote_url.startswith("http"):
5859
return remote_url
59-
else:
60-
raise ValueError('Cannot parse remote "{}" and transform to url'.format(remote_url))
60+
if url := _transform_ssh_remote_to_https(remote_url):
61+
return url
62+
63+
raise ValueError('Cannot parse remote "{}" and transform to url'.format(remote_url))
6164

6265

6366
def parse_remote(remote_url: str) -> GitHubRepo:
@@ -77,6 +80,51 @@ def parse_remote(remote_url: str) -> GitHubRepo:
7780
return GitHubRepo(url, fqdn, owner, repo, token)
7881

7982

83+
def _transform_ssh_remote_to_https(remote_url: str) -> str | None:
84+
match = re.match(r"^ssh://(?:[^@/]+@)?(?P<host>[^/]+)/(?P<path>.+)$", remote_url)
85+
if not match:
86+
if "://" in remote_url:
87+
return None
88+
match = re.match(r"^(?:[^@/:]+@)?(?P<host>[^:]+):(?P<path>.+)$", remote_url)
89+
if not match:
90+
return None
91+
92+
host, path = match.group("host"), match.group("path").lstrip("/")
93+
if "/" not in path:
94+
return None
95+
96+
return "https://{host}/{path}".format(host=_resolve_ssh_hostname(host), path=path)
97+
98+
99+
@lru_cache(maxsize=128)
100+
def _resolve_ssh_hostname(hostname: str) -> str:
101+
return _read_ssh_config_hostname(hostname) or hostname
102+
103+
104+
def _read_ssh_config_hostname(hostname: str) -> str | None:
105+
try:
106+
output = subprocess.check_output(
107+
["ssh", "-G", hostname],
108+
stderr=subprocess.DEVNULL,
109+
text=True,
110+
timeout=1.0,
111+
startupinfo=STARTUPINFO,
112+
)
113+
except (OSError, subprocess.SubprocessError):
114+
return None
115+
116+
for line in output.splitlines():
117+
parts = line.split(maxsplit=1)
118+
if len(parts) != 2:
119+
continue
120+
121+
key, value = parts
122+
if key.lower() == "hostname" and value:
123+
return value.strip()
124+
125+
return None
126+
127+
80128
def construct_github_file_url(rel_path, remote_url, commit_hash, start_line=None, end_line=None) -> str:
81129
"""
82130
Open the URL corresponding to the provided `rel_path` on `remote_url`.

tests/test_github.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from unittesting import DeferrableTestCase
2+
3+
from GitSavvy.github import github
4+
from GitSavvy.tests.mockito import unstub, when
5+
from GitSavvy.tests.parameterized import parameterized as p
6+
7+
8+
class TestGitHubRemoteParsing(DeferrableTestCase):
9+
def tearDown(self):
10+
unstub()
11+
github.remote_to_url.cache_clear()
12+
github._resolve_ssh_hostname.cache_clear()
13+
14+
@p.expand([
15+
("git://github.com/timbrel/GitSavvy.git", "https://github.com/timbrel/GitSavvy"),
16+
("https://github.com/timbrel/GitSavvy.git", "https://github.com/timbrel/GitSavvy"),
17+
("https://github.com/timbrel/GitSavvy", "https://github.com/timbrel/GitSavvy"),
18+
])
19+
def test_remote_to_url_non_ssh_remotes(self, remote_url, expected):
20+
self.assertEqual(expected, github.remote_to_url(remote_url))
21+
22+
def test_remote_to_url_resolves_host_alias_for_scp_like_ssh_remote(self):
23+
when(github)._resolve_ssh_hostname("my-github").thenReturn("github.com")
24+
25+
actual = github.remote_to_url("git@my-github:me/my-project.git")
26+
27+
self.assertEqual("https://github.com/me/my-project", actual)
28+
29+
def test_remote_to_url_resolves_host_alias_for_ssh_scheme_remote(self):
30+
when(github)._resolve_ssh_hostname("my-github").thenReturn("github.com")
31+
32+
actual = github.remote_to_url("ssh://git@my-github/me/my-project.git")
33+
34+
self.assertEqual("https://github.com/me/my-project", actual)
35+
36+
def test_read_ssh_config_hostname(self):
37+
when(github.subprocess).check_output(
38+
["ssh", "-G", "my-github"],
39+
stderr=github.subprocess.DEVNULL,
40+
text=True,
41+
timeout=1.0,
42+
startupinfo=github.STARTUPINFO,
43+
).thenReturn("host my-github\nhostname github.com\n")
44+
45+
actual = github._read_ssh_config_hostname("my-github")
46+
47+
self.assertEqual("github.com", actual)
48+
49+
def test_resolve_ssh_hostname_falls_back_to_input_value(self):
50+
when(github)._read_ssh_config_hostname("my-github").thenReturn(None)
51+
52+
actual = github._resolve_ssh_hostname("my-github")
53+
54+
self.assertEqual("my-github", actual)

unittesting.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"deferred": true,
33
"capture_console": true,
4-
"legacy_runner": false,
5-
"failfast": true
4+
"legacy_runner": false
65
}

0 commit comments

Comments
 (0)