7
7
import os
8
8
import re
9
9
import sys
10
+ from contextlib import contextmanager
10
11
from pathlib import Path
11
- from typing import TYPE_CHECKING , Final , Iterable
12
+ from typing import TYPE_CHECKING , Final , Generator , Iterable
12
13
from urllib .parse import urlparse , urlunparse
13
14
14
15
import git
@@ -217,13 +218,6 @@ async def fetch_remote_branches_or_tags(url: str, *, ref_type: str, token: str |
217
218
218
219
# Use GitPython to get remote references
219
220
try :
220
- git_cmd = git .Git ()
221
-
222
- # Prepare authentication if needed
223
- if token and is_github_host (url ):
224
- auth_url = _add_token_to_url (url , token )
225
- url = auth_url
226
-
227
221
fetch_tags = ref_type == "tags"
228
222
to_fetch = "tags" if fetch_tags else "heads"
229
223
@@ -233,8 +227,11 @@ async def fetch_remote_branches_or_tags(url: str, *, ref_type: str, token: str |
233
227
cmd_args .append ("--refs" ) # Filter out peeled tag objects
234
228
cmd_args .append (url )
235
229
236
- # Run the command using git_cmd.ls_remote() method
237
- output = git_cmd .ls_remote (* cmd_args )
230
+ # Run the command with proper authentication
231
+ with git_auth_context (url , token ) as (git_cmd , auth_url ):
232
+ # Replace the URL in cmd_args with the authenticated URL
233
+ cmd_args [- 1 ] = auth_url # URL is the last argument
234
+ output = git_cmd .ls_remote (* cmd_args )
238
235
239
236
# Parse output
240
237
return [
@@ -318,6 +315,70 @@ def create_git_auth_header(token: str, url: str = "https://github.com") -> str:
318
315
return f"http.https://{ hostname } /.extraheader=Authorization: Basic { basic } "
319
316
320
317
318
+ def create_authenticated_url (url : str , token : str | None = None ) -> str :
319
+ """Create an authenticated URL for Git operations.
320
+
321
+ This is the safest approach for multi-user environments - no global state.
322
+
323
+ Parameters
324
+ ----------
325
+ url : str
326
+ The repository URL.
327
+ token : str | None
328
+ GitHub personal access token (PAT) for accessing private repositories.
329
+
330
+ Returns
331
+ -------
332
+ str
333
+ The URL with authentication embedded (for GitHub) or original URL.
334
+
335
+ """
336
+ if not (token and is_github_host (url )):
337
+ return url
338
+
339
+ parsed = urlparse (url )
340
+ # Add token as username in URL (GitHub supports this)
341
+ netloc = f"x-oauth-basic:{ token } @{ parsed .hostname } "
342
+ if parsed .port :
343
+ netloc += f":{ parsed .port } "
344
+
345
+ return urlunparse (
346
+ (
347
+ parsed .scheme ,
348
+ netloc ,
349
+ parsed .path ,
350
+ parsed .params ,
351
+ parsed .query ,
352
+ parsed .fragment ,
353
+ ),
354
+ )
355
+
356
+
357
+ @contextmanager
358
+ def git_auth_context (url : str , token : str | None = None ) -> Generator [tuple [git .Git , str ]]:
359
+ """Context manager that provides Git command and authenticated URL.
360
+
361
+ Returns both a Git command object and the authenticated URL to use.
362
+ This avoids any global state contamination between users.
363
+
364
+ Parameters
365
+ ----------
366
+ url : str
367
+ The repository URL to check if authentication is needed.
368
+ token : str | None
369
+ GitHub personal access token (PAT) for accessing private repositories.
370
+
371
+ Yields
372
+ ------
373
+ Generator[tuple[git.Git, str]]
374
+ Tuple of (Git command object, authenticated URL to use).
375
+
376
+ """
377
+ git_cmd = git .Git ()
378
+ auth_url = create_authenticated_url (url , token )
379
+ yield git_cmd , auth_url
380
+
381
+
321
382
def validate_github_token (token : str ) -> None :
322
383
"""Validate the format of a GitHub Personal Access Token.
323
384
@@ -419,15 +480,9 @@ async def _resolve_ref_to_sha(url: str, pattern: str, token: str | None = None)
419
480
420
481
"""
421
482
try :
422
- git_cmd = git .Git ()
423
-
424
- # Prepare authentication if needed
425
- auth_url = url
426
- if token and is_github_host (url ):
427
- auth_url = _add_token_to_url (url , token )
428
-
429
- # Execute ls-remote command
430
- output = git_cmd .ls_remote (auth_url , pattern )
483
+ # Execute ls-remote command with proper authentication
484
+ with git_auth_context (url , token ) as (git_cmd , auth_url ):
485
+ output = git_cmd .ls_remote (auth_url , pattern )
431
486
lines = output .splitlines ()
432
487
433
488
sha = _pick_commit_sha (lines )
@@ -475,37 +530,3 @@ def _pick_commit_sha(lines: Iterable[str]) -> str | None:
475
530
first_non_peeled = sha
476
531
477
532
return first_non_peeled # branch or lightweight tag (or None)
478
-
479
-
480
- def _add_token_to_url (url : str , token : str ) -> str :
481
- """Add authentication token to GitHub URL.
482
-
483
- Parameters
484
- ----------
485
- url : str
486
- The original GitHub URL.
487
- token : str
488
- The GitHub token to add.
489
-
490
- Returns
491
- -------
492
- str
493
- The URL with embedded authentication.
494
-
495
- """
496
- parsed = urlparse (url )
497
- # Add token as username in URL (GitHub supports this)
498
- netloc = f"x-oauth-basic:{ token } @{ parsed .hostname } "
499
- if parsed .port :
500
- netloc += f":{ parsed .port } "
501
-
502
- return urlunparse (
503
- (
504
- parsed .scheme ,
505
- netloc ,
506
- parsed .path ,
507
- parsed .params ,
508
- parsed .query ,
509
- parsed .fragment ,
510
- ),
511
- )
0 commit comments