@@ -31,6 +31,89 @@ class CloneConfig:
3131 local_path : str
3232 commit : str | None = None
3333 branch : str | None = None
34+ pat : str | None = None
35+
36+
37+ async def _check_repo_exists (url : str , pat : str | None = None ) -> bool :
38+ """
39+ Check if a repository exists at the given URL using an HTTP HEAD request.
40+
41+ Parameters
42+ ----------
43+ url : str
44+ The URL of the repository.
45+ pat : str | None
46+ Personal Access Token for authentication, optional.
47+
48+ Returns
49+ -------
50+ bool
51+ True if the repository exists, False otherwise.
52+ """
53+ # Parse URL to get components
54+ parts = url .split ('/' )
55+ if len (parts ) < 5 : # Need at least protocol, empty, host, username, repo
56+ return False
57+
58+ host = parts [2 ]
59+ username = parts [3 ]
60+ repo = parts [4 ]
61+
62+ # Construct API URL based on host
63+ if 'github.com' in host :
64+ api_url = url
65+ else :
66+ # For custom Git servers, use API v1 endpoint
67+ api_url = f"https://{ host } /api/v1/repos/{ username } /{ repo } "
68+
69+ cmd = ["curl" , "-I" ]
70+ if pat :
71+ cmd .extend (["-H" , f"Authorization: token { pat } " ])
72+ cmd .append (api_url )
73+
74+ proc = await asyncio .create_subprocess_exec (
75+ * cmd ,
76+ stdout = asyncio .subprocess .PIPE ,
77+ stderr = asyncio .subprocess .PIPE ,
78+ )
79+ stdout , _ = await proc .communicate ()
80+ if proc .returncode != 0 :
81+ return False
82+ # Check if stdout contains "404" status code
83+ stdout_str = stdout .decode ()
84+ return "HTTP/1.1 404" not in stdout_str and "HTTP/2 404" not in stdout_str
85+
86+
87+ async def _run_git_command (* args : str ) -> tuple [bytes , bytes ]:
88+ """
89+ Executes a git command asynchronously and captures its output.
90+
91+ Parameters
92+ ----------
93+ *args : str
94+ The git command and its arguments to execute.
95+
96+ Returns
97+ -------
98+ tuple[bytes, bytes]
99+ A tuple containing the stdout and stderr of the git command.
100+
101+ Raises
102+ ------
103+ RuntimeError
104+ If the git command exits with a non-zero status.
105+ """
106+ proc = await asyncio .create_subprocess_exec (
107+ * args ,
108+ stdout = asyncio .subprocess .PIPE ,
109+ stderr = asyncio .subprocess .PIPE ,
110+ )
111+ stdout , stderr = await proc .communicate ()
112+ if proc .returncode != 0 :
113+ error_message = stderr .decode ().strip ()
114+ raise RuntimeError (f"Git command failed: { ' ' .join (args )} \n Error: { error_message } " )
115+
116+ return stdout , stderr
34117
35118
36119@async_timeout (CLONE_TIMEOUT )
@@ -45,11 +128,12 @@ async def clone_repo(config: CloneConfig) -> tuple[bytes, bytes]:
45128 Parameters
46129 ----------
47130 config : CloneConfig
48- A dictionary containing the following keys :
131+ Configuration object containing:
49132 - url (str): The URL of the repository.
50133 - local_path (str): The local path to clone the repository to.
51134 - commit (Optional[str]): The specific commit hash to checkout.
52135 - branch (Optional[str]): The branch to clone. Defaults to 'main' or 'master' if not provided.
136+ - pat (Optional[str]): Personal Access Token for authentication.
53137
54138 Returns
55139 -------
@@ -65,11 +149,12 @@ async def clone_repo(config: CloneConfig) -> tuple[bytes, bytes]:
65149 AsyncTimeoutError
66150 If the cloning process exceeds the specified timeout.
67151 """
68- # Extract and validate query parameters
152+ # Extract and validate parameters
69153 url : str = config .url
70154 local_path : str = config .local_path
71155 commit : str | None = config .commit
72156 branch : str | None = config .branch
157+ pat : str | None = config .pat
73158
74159 if not url :
75160 raise ValueError ("The 'url' parameter is required." )
@@ -78,13 +163,15 @@ async def clone_repo(config: CloneConfig) -> tuple[bytes, bytes]:
78163 raise ValueError ("The 'local_path' parameter is required." )
79164
80165 # Check if the repository exists
81- if not await _check_repo_exists (url ):
82- raise ValueError ("Repository not found, make sure it is public" )
166+ if not await _check_repo_exists (url , pat ):
167+ raise ValueError ("Repository not found, make sure it is public or provide valid PAT " )
83168
84169 try :
85170 if commit :
86171 # Scenario 1: Clone and checkout a specific commit
87172 # Clone the repository without depth to ensure full history for checkout
173+ if pat :
174+ url = url .replace ("https://" , f"https://oauth2:{ pat } @" )
88175 clone_cmd = ["git" , "clone" , "--single-branch" , url , local_path ]
89176 await _run_git_command (* clone_cmd )
90177
@@ -93,75 +180,17 @@ async def clone_repo(config: CloneConfig) -> tuple[bytes, bytes]:
93180 return await _run_git_command (* checkout_cmd )
94181
95182 if branch and branch .lower () not in ("main" , "master" ):
96-
97183 # Scenario 2: Clone a specific branch with shallow depth
184+ if pat :
185+ url = url .replace ("https://" , f"https://oauth2:{ pat } @" )
98186 clone_cmd = ["git" , "clone" , "--depth=1" , "--single-branch" , "--branch" , branch , url , local_path ]
99187 return await _run_git_command (* clone_cmd )
100188
101189 # Scenario 3: Clone the default branch with shallow depth
190+ if pat :
191+ url = url .replace ("https://" , f"https://oauth2:{ pat } @" )
102192 clone_cmd = ["git" , "clone" , "--depth=1" , "--single-branch" , url , local_path ]
103193 return await _run_git_command (* clone_cmd )
104194
105195 except (RuntimeError , asyncio .TimeoutError , AsyncTimeoutError ):
106196 raise # Re-raise the exception
107-
108-
109- async def _check_repo_exists (url : str ) -> bool :
110- """
111- Check if a repository exists at the given URL using an HTTP HEAD request.
112-
113- Parameters
114- ----------
115- url : str
116- The URL of the repository.
117-
118- Returns
119- -------
120- bool
121- True if the repository exists, False otherwise.
122- """
123- proc = await asyncio .create_subprocess_exec (
124- "curl" ,
125- "-I" ,
126- url ,
127- stdout = asyncio .subprocess .PIPE ,
128- stderr = asyncio .subprocess .PIPE ,
129- )
130- stdout , _ = await proc .communicate ()
131- if proc .returncode != 0 :
132- return False
133- # Check if stdout contains "404" status code
134- stdout_str = stdout .decode ()
135- return "HTTP/1.1 404" not in stdout_str and "HTTP/2 404" not in stdout_str
136-
137-
138- async def _run_git_command (* args : str ) -> tuple [bytes , bytes ]:
139- """
140- Executes a git command asynchronously and captures its output.
141-
142- Parameters
143- ----------
144- *args : str
145- The git command and its arguments to execute.
146-
147- Returns
148- -------
149- tuple[bytes, bytes]
150- A tuple containing the stdout and stderr of the git command.
151-
152- Raises
153- ------
154- RuntimeError
155- If the git command exits with a non-zero status.
156- """
157- proc = await asyncio .create_subprocess_exec (
158- * args ,
159- stdout = asyncio .subprocess .PIPE ,
160- stderr = asyncio .subprocess .PIPE ,
161- )
162- stdout , stderr = await proc .communicate ()
163- if proc .returncode != 0 :
164- error_message = stderr .decode ().strip ()
165- raise RuntimeError (f"Git command failed: { ' ' .join (args )} \n Error: { error_message } " )
166-
167- return stdout , stderr
0 commit comments