11import json
22import logging
33import os
4- import re
54import shutil
65from abc import abstractmethod
76from collections .abc import Iterable , Iterator
109from tempfile import NamedTemporaryFile
1110from time import time
1211from typing import TYPE_CHECKING , Any , Optional
12+ from urllib .parse import urlparse
1313
1414import aiohttp
1515from aiohttp_retry import ExponentialRetry , RetryClient
2020
2121from scmrepo .git .backend .dulwich import _get_ssh_vendor
2222from scmrepo .git .credentials import Credential , CredentialNotFoundError
23+ from scmrepo .urls import SCP_REGEX , is_scp_style_url
2324
2425from .exceptions import LFSError
2526from .pointer import Pointer
@@ -84,7 +85,7 @@ def loop(self):
8485
8586 @classmethod
8687 def from_git_url (cls , git_url : str ) -> "LFSClient" :
87- if git_url .startswith (( "ssh://" , "git@" ) ):
88+ if git_url .startswith ("ssh://" ) or is_scp_style_url ( git_url ):
8889 return _SSHLFSClient .from_git_url (git_url )
8990 if git_url .startswith (("http://" , "https://" )):
9091 return _HTTPLFSClient .from_git_url (git_url )
@@ -213,11 +214,9 @@ def _get_auth_header(self, *, upload: bool) -> dict:
213214
214215
215216class _SSHLFSClient (LFSClient ):
216- _URL_PATTERN = re .compile (
217- r"(?:ssh://)?git@(?P<host>\S+?)(?::(?P<port>\d+))?(?:[:/])(?P<path>\S+?)\.git"
218- )
219-
220- def __init__ (self , url : str , host : str , port : int , path : str ):
217+ def __init__ (
218+ self , url : str , host : str , port : int , username : Optional [str ], path : str
219+ ):
221220 """
222221 Args:
223222 url: LFS server URL.
@@ -228,33 +227,50 @@ def __init__(self, url: str, host: str, port: int, path: str):
228227 super ().__init__ (url )
229228 self .host = host
230229 self .port = port
230+ self .username = username
231231 self .path = path
232232 self ._ssh = _get_ssh_vendor ()
233233
234234 @classmethod
235235 def from_git_url (cls , git_url : str ) -> "_SSHLFSClient" :
236- result = cls ._URL_PATTERN .match (git_url )
237- if not result :
236+ if scp_match := SCP_REGEX .match (git_url ):
237+ # Add an ssh:// prefix and replace the ':' with a '/'.
238+ git_url = scp_match .expand (r"ssh://\1\2/\3" )
239+
240+ parsed = urlparse (git_url )
241+ if parsed .scheme != "ssh" or not parsed .hostname :
238242 raise ValueError (f"Invalid Git SSH URL: { git_url } " )
239- host , port , path = result .group ("host" , "port" , "path" )
240- url = f"https://{ host } /{ path } .git/info/lfs"
241- return cls (url , host , int (port or 22 ), path )
243+
244+ host = parsed .hostname
245+ port = parsed .port or 22
246+ path = parsed .path .lstrip ("/" )
247+ username = parsed .username
248+
249+ url_path = path .removesuffix (".git" ) + ".git/info/lfs"
250+ url = f"https://{ host } /{ url_path } "
251+ return cls (url , host , port , username , path )
242252
243253 def _get_auth_header (self , * , upload : bool ) -> dict :
244254 return self ._git_lfs_authenticate (
245- self .host , self .port , f" { self .path } .git" , upload = upload
255+ self .host , self .port , self .username , self . path , upload = upload
246256 ).get ("header" , {})
247257
248258 def _git_lfs_authenticate (
249- self , host : str , port : int , path : str , * , upload : bool = False
259+ self ,
260+ host : str ,
261+ port : int ,
262+ username : Optional [str ],
263+ path : str ,
264+ * ,
265+ upload : bool = False ,
250266 ) -> dict :
251267 action = "upload" if upload else "download"
252268 return json .loads (
253269 self ._ssh .run_command (
254270 command = f"git-lfs-authenticate { path } { action } " ,
255271 host = host ,
256272 port = port ,
257- username = "git" ,
273+ username = username ,
258274 ).read ()
259275 )
260276
0 commit comments