55from __future__ import annotations
66import os
77import 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
1010from typing import NamedTuple
11+ from webbrowser import open as open_in_browser
1112
1213from ..common import interwebs
1314from ..core .exceptions import FailedGithubRequest
1415from ..core .settings import GitSavvySettings
16+ from ..core .utils import STARTUPINFO
1517
1618
1719GITHUB_PER_PAGE_MAX = 100
@@ -33,14 +35,15 @@ class GitHubRepo(NamedTuple):
3335 token : str | None
3436
3537
38+ @lru_cache (maxsize = 128 )
3639def 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
6366def 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+
80128def 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`.
0 commit comments